/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt VS Tools.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
namespace QtVsTools.Json
{
///
/// Classes in an 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]
abstract class Serializable :
Prototyped,
IDeferrable,
IDeferredObjectContainer
where TBase : Serializable
{
#region //////////////////// Prototype ////////////////////////////////////////////////////
private Serializer Serializer { get; set; }
protected Serializable()
{ }
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.
///
///
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 /////////////////////////////////////////////
List deferredObjects = null;
List DeferredObjects
{
get
{
Atomic(() => deferredObjects == null,
() => deferredObjects = new List());
return deferredObjects;
}
}
public IEnumerable PendingObjects
{
get
{
return 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()
{
return ThreadSafe(() => Prototype.Serializer.Serialize(this).GetBytes());
}
///
/// 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)
{
TBase 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 sucessful 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 sucessful 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 sub-classes
while (toDo.Count > 0) {
var subClass = toDo.Dequeue();
// Try to deserialize as sub-class
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 compatbility
var isCompatible = subClass.Prototype.IsCompatible(tryObj);
if (isCompatible == false) {
// Incompatible
continue;
} else 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;
} else {
// Maybe has compatible derived class
if (subClass.SubTypes.Any()) {
// Non-leaf node; continue searching
PotentialSubClasses(subClass, tryObj)
.ForEach(x => toDo.Enqueue(x));
}
continue;
}
}
// No compatible leaf node found
// Use last successful (non-leaf) deserializtion, if any
return lastCompatibleObj;
}
}
///
/// Get list of sub-classes of a particular class that are potentially suitable to the
/// deserialized data. Sub-classes 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 sub-classes are to be tested
/// Deserialized data
/// List of sub-classes that are potentially suitable for deserialization
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;
}
}
class SkipDeserializationAttribute : Attribute
{ }
}