// Copyright (C) 2025 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; namespace QtVsTools.Json { /// /// Classes in a hierarchy derived from Serializable represent objects that can be mapped /// to and from JSON data using the DataContractJsonSerializer class. When deserializing, the /// class hierarchy will be searched for the derived class best suited to the data. /// /// Base of the class hierarchy [DataContract] public abstract class Serializable : Prototyped, IDeferrable, IDeferredObjectContainer where TBase : Serializable { #region //////////////////// Prototype //////////////////////////////////////////////////// private Serializer Serializer { get; set; } protected sealed override void InitializePrototype() { System.Diagnostics.Debug.Assert(IsPrototype); // Create serializer for this particular type Serializer = Serializer.Create(GetType()); } /// /// Check if this class is suited as target type for deserialization, based on the /// information already deserialized. Prototypes of derived classes will override this to /// implement the local type selection rules. /// /// Object containing the data deserialized so far /// /// true ::= class is suitable and can be used as target type for deserialization /// /// false ::= class is not suitable for deserialization /// /// null ::= a derived class of this class might be suitable; search for target type /// should be expanded to include all classes derived from this class /// /// protected virtual bool? IsCompatible(TBase that) { System.Diagnostics.Debug.Assert(IsPrototype); return null; } /// /// Check if this class is marked with the [SkipDeserialization] attribute, which signals /// that deserialization of this class is to be skipped while traversing the class /// hierarchy looking for a suitable target type for deserialization. /// /// private bool SkipDeserialization { get { System.Diagnostics.Debug.Assert(IsPrototype); return GetType() .GetCustomAttributes(typeof(SkipDeserializationAttribute), false).Any(); } } /// /// Perform a deferred deserialization based on this class hierarchy. /// /// Data to deserialize /// Deserialized object /// TBase IDeferrable.Deserialize(IJsonData jsonData) { System.Diagnostics.Debug.Assert(this == BasePrototype); return DeserializeClassHierarchy(null, jsonData); } #endregion //////////////////// Prototype ///////////////////////////////////////////////// #region //////////////////// Deferred Objects ///////////////////////////////////////////// private List deferredObjects; private List DeferredObjects { get { Atomic(() => deferredObjects == null, () => deferredObjects = new List()); return deferredObjects; } } public IEnumerable PendingObjects => DeferredObjects.Where(x => !x.HasData); void IDeferredObjectContainer.Add(IDeferredObject item) { ThreadSafe(() => DeferredObjects.Add(item)); } protected void Add(IDeferredObject item) { ((IDeferredObjectContainer)this).Add(item); } #endregion //////////////////// Deferred Objects ////////////////////////////////////////// /// /// Initialize new instance. Derived classes override this to implement their own /// initializations. /// /// protected virtual void InitializeObject(object initArgs) { } /// /// Serialize object. /// /// Raw JSON data /// public byte[] Serialize(bool indent = false) { return ThreadSafe(() => Prototype.Serializer.Serialize(this, indent).GetBytes()); } /// /// Serialize object. /// /// JSON string /// public string ToJsonString(bool indent = true) { return ThreadSafe(() => Prototype.Serializer.Serialize(this, indent).GetString()); } /// /// Deserialize object using this class hierarchy. After selecting the most suitable derived /// class as target type and deserializing an instance of that class, any deferred objects /// are also deserialized using their respective class hierarchies. /// /// Additional arguments required for object initialization /// Raw JSON data /// Deserialized object, or null if deserialization failed /// public static TBase Deserialize(object initArgs, byte[] data) { var obj = DeserializeClassHierarchy(initArgs, Serializer.Parse(data)); if (obj == null) return null; var toDo = new Queue(); if (obj.PendingObjects.Any()) toDo.Enqueue(obj); while (toDo.Count > 0) { var container = toDo.Dequeue(); foreach (var defObj in container.PendingObjects) { defObj.Deserialize(); if (defObj.Object is IDeferredObjectContainer subContainer && subContainer.PendingObjects.Any()) { toDo.Enqueue(subContainer); } } } return obj; } public static TBase Deserialize(byte[] data) { return Deserialize(null, data); } /// /// Traverse this class hierarchy looking for the most suitable derived class that can be /// used as target type for the deserialization of the JSON data provided. /// /// Additional arguments required for object initialization /// Parsed JSON data /// Deserialized object, or null if deserialization failed /// protected static TBase DeserializeClassHierarchy(object initArgs, IJsonData jsonData) { // PSEUDOCODE: // // Nodes to visit := base of class hierarchy. // While there are still nodes to visit // Current node ::= Extract next node to visit. // Tentative object := Deserialize using current node as target type. // If deserialization failed // Skip branch, continue (with next node, if any). // Else // Test compatibility of current node with tentative object. // If not compatible // Skip branch, continue (with next node, if any). // If compatible // If leaf node // Found suitable node!! // Return tentative object as final result of deserialization. // Else // Save tentative object as last successful deserialization. // Add child nodes to the nodes to visit. // If inconclusive (i.e. a child node might be compatible) // Add child nodes to the nodes to visit. // If no suitable node was found // Return last successful deserialization as final result of deserialization. lock (BaseClass.Prototype.CriticalSection) { var toDo = new Queue(new[] { BaseClass }); TBase lastCompatibleObj = null; // Traverse class hierarchy tree looking for a compatible leaf node // i.e. compatible class without any subclasses while (toDo.Count > 0) { var subClass = toDo.Dequeue(); // Try to deserialize as subclass TBase tryObj; if (jsonData.IsEmpty()) tryObj = CreateInstance(subClass.Type); else tryObj = subClass.Prototype.Serializer.Deserialize(jsonData) as TBase; if (tryObj == null) continue; // Not deserializable as this type tryObj.InitializeObject(initArgs); // Test compatibility var isCompatible = subClass.Prototype.IsCompatible(tryObj); if (isCompatible == false) continue; // Incompatible if (isCompatible == true) { // Compatible if (!subClass.SubTypes.Any()) return tryObj; // Found compatible leaf node! // Non-leaf node; continue searching lastCompatibleObj = tryObj; PotentialSubClasses(subClass, tryObj) .ForEach(x => toDo.Enqueue(x)); continue; } // Maybe has compatible derived class if (subClass.SubTypes.Any()) { // Non-leaf node; continue searching PotentialSubClasses(subClass, tryObj) .ForEach(x => toDo.Enqueue(x)); } } // No compatible leaf node found // Use last successful (non-leaf) deserialization, if any return lastCompatibleObj; } } /// /// Get list of subclasses of a particular class that are potentially suitable to the /// deserialized data. Subclasses marked with the [SkipDeserialization] attribute will not /// be returned; their own sub-sub-classes will be tested for compatibility and returned in /// case they are potentially suitable (i.e.: IsCompatible == true || IsCompatible == null) /// /// Class whose subclasses are to be tested /// Deserialized data /// List of subclasses that are potentially suitable for deserialization private static List PotentialSubClasses(SubClass subClass, TBase tryObj) { if (subClass == null || tryObj == null) return new List(); var potential = new List(); var toDo = new Queue(subClass.SubClasses); while (toDo.Count > 0) { subClass = toDo.Dequeue(); if (subClass.Prototype.IsCompatible(tryObj) == false) continue; if (subClass.Prototype.SkipDeserialization && subClass.SubClasses.Any()) { foreach (var subSubClass in subClass.SubClasses) toDo.Enqueue(subSubClass); continue; } potential.Add(subClass); } return potential; } } public class SkipDeserializationAttribute : Attribute { } }