Skip to content

Commit

Permalink
Added SharedObjects serialization to the model;
Browse files Browse the repository at this point in the history
  • Loading branch information
katehryhorenko committed Jan 16, 2025
1 parent 114dbb4 commit 73ed8d6
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 18 deletions.
7 changes: 7 additions & 0 deletions Elements/src/Element.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ protected virtual void RaisePropertyChanged([System.Runtime.CompilerServices.Cal
[JsonProperty("Mappings", Required = Required.Default, NullValueHandling = NullValueHandling.Ignore)]
internal Dictionary<string, MappingBase> Mappings { get; set; } = null;

/// <summary>
/// An optional shared object that can be used to share data between elements.
/// </summary>
/// <value></value>
[JsonProperty("SharedObject", Required = Required.Default, NullValueHandling = NullValueHandling.Ignore)]
public SharedObject SharedObject { get; set; }

/// <summary>
/// The method used to set a mapping for a given context.
/// </summary>
Expand Down
85 changes: 68 additions & 17 deletions Elements/src/Model.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public class Model
[System.ComponentModel.DataAnnotations.Required]
public System.Collections.Generic.IDictionary<Guid, Element> Elements { get; set; } = new System.Collections.Generic.Dictionary<Guid, Element>();

/// <summary>A collection of SharedObjects keyed by their identifiers.</summary>
[JsonProperty("SharedObjects", Required = Required.Default)]
[System.ComponentModel.DataAnnotations.Required]
public System.Collections.Generic.IDictionary<Guid, SharedObject> SharedObjects { get; set; } = new System.Collections.Generic.Dictionary<Guid, SharedObject>();

/// <summary>
/// Collection of subelements from shared objects or RepresentationInstances (e.g. SolidRepresentation.Profile or RepresentationInstance.Material).
/// We do not serialize shared objects to json, but we do include them in other formats like gltf.
Expand Down Expand Up @@ -123,7 +128,7 @@ public void AddElement(Element element, bool gatherSubElements = true, bool upda
// to the elements dictionary first. This will ensure that
// those elements will be read out and be available before
// an attempt is made to deserialize the element itself.
var subElements = RecursiveGatherSubElements(element, out var elementsToIgnore);
var (subElements, sharedObjects) = RecursiveGatherSubElements(element, out var elementsToIgnore);
foreach (var e in subElements)
{
if (!this.Elements.ContainsKey(e.Id))
Expand All @@ -138,6 +143,14 @@ public void AddElement(Element element, bool gatherSubElements = true, bool upda
}
}

foreach (var sharedObject in sharedObjects)
{
if (!SharedObjects.ContainsKey(sharedObject.Id))
{
SharedObjects.Add(sharedObject.Id, sharedObject);
}
}

foreach (var e in elementsToIgnore)
{
if (!SubElementsFromSharedObjects.ContainsKey(e.Id))
Expand Down Expand Up @@ -453,23 +466,24 @@ public static Model FromJson(string json, bool forceTypeReload = false)
return FromJson(json, out _, forceTypeReload);
}

private List<Element> RecursiveGatherSubElements(object obj, out List<Element> elementsToIgnore)
private (List<Element> elements, List<SharedObject> sharedObjects) RecursiveGatherSubElements(object obj, out List<Element> sharedObjectSubElements)
{
// A dictionary created for the purpose of caching properties
// that we need to recurse, for types that we've seen before.
var props = new Dictionary<Type, List<PropertyInfo>>();

return RecursiveGatherSubElementsInternal(obj, props, out elementsToIgnore);
return RecursiveGatherSubElementsInternal(obj, props, out sharedObjectSubElements);
}

private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<Type, List<PropertyInfo>> properties, out List<Element> elementsToIgnore)
private (List<Element> elements, List<SharedObject> sharedObjects) RecursiveGatherSubElementsInternal(object obj, Dictionary<Type, List<PropertyInfo>> properties, out List<Element> sharedObjectSubElements)
{
var elements = new List<Element>();
elementsToIgnore = new List<Element>();
sharedObjectSubElements = new List<Element>();
var sharedObjects = new List<SharedObject>();

if (obj == null)
{
return elements;
return (elements, sharedObjects);
}

var e = obj as Element;
Expand All @@ -478,7 +492,7 @@ private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<
// Do nothing. The Element has already
// been added. This assumes that that the sub-elements
// have been added as well and we don't need to continue.
return elements;
return (elements, sharedObjects);
}

// This explicit loop is because we have mappings marked as internal so it's elements won't be automatically serialized.
Expand All @@ -491,14 +505,24 @@ private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<
}
}

var sharedObject = obj as SharedObject;

if (sharedObject != null)
{
if (SharedObjects.ContainsKey(sharedObject.Id))
{
return (elements, sharedObjects);
}
}

var t = obj.GetType();

// Ignore value types and strings
// as they won't have properties that
// could be elements.
if (!t.IsClass || t == typeof(string))
{
return elements;
return (elements, sharedObjects);
}

List<PropertyInfo> constrainedProps;
Expand All @@ -516,6 +540,7 @@ private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<
}

var elementsFromProperties = new List<Element>();
var sharedObjectsFromProperties = new List<SharedObject>();
foreach (var p in constrainedProps)
{
try
Expand All @@ -526,12 +551,21 @@ private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<
continue;
}

// Do not save shared object to the model if it is marked with JsonIgnore (e.g. ElementRepresentation)
bool hasJsonIgnore = p.GetCustomAttributes(typeof(JsonIgnoreAttribute), true).Any();

if (pValue is IList elems)
{
foreach (var item in elems)
{
elementsFromProperties.AddRange(RecursiveGatherSubElementsInternal(item, properties, out var elementsFromItemToIgnore));
elementsToIgnore.AddRange(elementsFromItemToIgnore);
var (subElements, subSharedObjects) = RecursiveGatherSubElementsInternal(item, properties, out var elementsFromSharedObjectProperties);
elementsFromProperties.AddRange(subElements);
// do not save shared objects marked with JsonIgnore
if (!hasJsonIgnore)
{
sharedObjectsFromProperties.AddRange(subSharedObjects);
}
sharedObjectSubElements.AddRange(elementsFromSharedObjectProperties);
}
continue;
}
Expand All @@ -541,14 +575,26 @@ private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<
{
foreach (var value in dict.Values)
{
elementsFromProperties.AddRange(RecursiveGatherSubElementsInternal(value, properties, out var elementsFromValueToIgnore));
elementsToIgnore.AddRange(elementsFromValueToIgnore);
var (subElements, subSharedObjects) = RecursiveGatherSubElementsInternal(value, properties, out var elementsFromSharedObjectProperties);
elementsFromProperties.AddRange(subElements);
// do not save shared objects marked with JsonIgnore
if (!hasJsonIgnore)
{
sharedObjectsFromProperties.AddRange(subSharedObjects);
}
sharedObjectSubElements.AddRange(elementsFromSharedObjectProperties);
}
continue;
}

elementsFromProperties.AddRange(RecursiveGatherSubElementsInternal(pValue, properties, out var elementsFromPropertyToIgnore));
elementsToIgnore.AddRange(elementsFromPropertyToIgnore);
var (gatheredSubElements, gatheredSubSharedObjects) = RecursiveGatherSubElementsInternal(pValue, properties, out var elementsFromPropertyToIgnore);
elementsFromProperties.AddRange(gatheredSubElements);
// do not save shared objects marked with JsonIgnore
if (!hasJsonIgnore)
{
sharedObjectsFromProperties.AddRange(gatheredSubSharedObjects);
}
sharedObjectSubElements.AddRange(elementsFromPropertyToIgnore);
}
catch (Exception ex)
{
Expand All @@ -557,19 +603,25 @@ private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<
}
if (IsTypeRelatedToSharedObjects(t))
{
elementsToIgnore.AddRange(elementsFromProperties);
sharedObjectSubElements.AddRange(elementsFromProperties);
}
else
{
elements.AddRange(elementsFromProperties);
}
sharedObjects.AddRange(sharedObjectsFromProperties);

if (e != null)
{
elements.Add(e);
}

return elements;
if (sharedObject != null)
{
sharedObjects.Add(sharedObject);
}

return (elements, sharedObjects);
}

/// <summary>
Expand Down Expand Up @@ -624,7 +676,6 @@ internal static bool IsValidForRecursiveAddition(Type t)

private static bool IsTypeRelatedToSharedObjects(Type t)
{

return typeof(SharedObject).IsAssignableFrom(t)
|| typeof(RepresentationInstance).IsAssignableFrom(t);
}
Expand Down
103 changes: 102 additions & 1 deletion Elements/src/Serialization/JSON/JsonInheritanceConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ public static Dictionary<Guid, Element> Elements
}
}

private static Dictionary<Guid, SharedObject> _sharedObjects = null;

public static Dictionary<Guid, SharedObject> SharedObjects
{
get
{
if (_sharedObjects == null)
{
_sharedObjects = new Dictionary<Guid, SharedObject>();
}
return _sharedObjects;
}
}

[System.ThreadStatic]
private static List<string> _deserializationWarnings;

Expand Down Expand Up @@ -170,11 +184,17 @@ public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value,

// Operate on all identifiable Elements with a path less than Entities.xxxxx
// This will get all properties.
if (value is Element element && !WritingTopLevelElement(writer.Path) && !ElementwiseSerialization)
var element = value as Element;
if (element != null && !WritingTopLevelElement(writer.Path) && !ElementwiseSerialization)
{
var ident = element;
writer.WriteValue(ident.Id);
}
else if (value is SharedObject sharedObject && !WritingTopLevelSharedObject(writer.Path) && !ElementwiseSerialization)
{
var ident = sharedObject;
writer.WriteValue(ident.Id);
}
else
{
var jObject = Newtonsoft.Json.Linq.JObject.FromObject(value, serializer);
Expand All @@ -195,6 +215,13 @@ public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value,
{
jObject.AddFirst(new Newtonsoft.Json.Linq.JProperty(_discriminator, discriminatorName));
}

// Remove properties that are the same as in SharedObject
if (element != null && element.SharedObject != null)
{
RemovePropertiesSameAsInSharedObject(element, jObject);

}
writer.WriteToken(jObject.CreateReader());
}
}
Expand All @@ -204,6 +231,50 @@ public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value,
}
}

private void RemovePropertiesSameAsInSharedObject(Element element, JObject jObject)
{
var sharedProperties = element.SharedObject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
var elementProperties = element.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);

foreach (var property in elementProperties)
{
// Check if property is in SharedObject
var sharedProperty = sharedProperties.FirstOrDefault(p => p.Name == property.Name);
if (sharedProperty != null)
{
var sharedValue = sharedProperty.GetValue(element.SharedObject);
var elementValue = property.GetValue(element);

// If property value in SharedObject and Element are the same, remove property from jObject
if (Equals(sharedValue, elementValue)) // Compare values
{
jObject.Remove(property.Name);
}
// If property has JsonExtensionDataAttribute (e.g. AdditionalProperties)
// compare each value in the dictionary
else if (Attribute.IsDefined(sharedProperty, typeof(JsonExtensionDataAttribute)))
{
if (sharedProperty.GetValue(element.SharedObject) is IDictionary<string, object> extraDataFromSharedObject
&& property.GetValue(element) is IDictionary<string, object> extraDataFromElement)
{
foreach (var extraDataFromSharedObjectKey in extraDataFromSharedObject.Keys)
{
if (string.Equals(extraDataFromSharedObjectKey, _discriminator))
{
continue;
}
if (extraDataFromElement.ContainsKey(extraDataFromSharedObjectKey) &&
Equals(extraDataFromSharedObject[extraDataFromSharedObjectKey], extraDataFromElement[extraDataFromSharedObjectKey]))
{
jObject.Remove(extraDataFromSharedObjectKey);
}
}
}
}
}
}
}

private static bool WritingTopLevelElement(string path)
{
var parts = path.Split('.');
Expand All @@ -214,6 +285,16 @@ private static bool WritingTopLevelElement(string path)
return false;
}

private static bool WritingTopLevelSharedObject(string path)
{
var parts = path.Split('.');
if (parts.Length == 2 && parts[0] == "SharedObjects" && Guid.TryParse(parts[1], out var _))
{
return true;
}
return false;
}

private static string GetDiscriminatorName(object value)
{
var t = value.GetType();
Expand Down Expand Up @@ -280,6 +361,17 @@ public override object ReadJson(Newtonsoft.Json.JsonReader reader, System.Type o
return Elements[id];
}

if (typeof(SharedObject).IsAssignableFrom(objectType) && !WritingTopLevelSharedObject(reader.Path) && reader.Value != null)
{
var id = Guid.Parse(reader.Value.ToString());
if (!SharedObjects.ContainsKey(id))
{
DeserializationWarnings.Add($"SharedObject {id} was not found during deserialization. Check for other deserialization errors.");
return null;
}
return SharedObjects[id];
}

var jObject = serializer.Deserialize<Newtonsoft.Json.Linq.JObject>(reader);
if (jObject == null)
{
Expand Down Expand Up @@ -322,6 +414,15 @@ public override object ReadJson(Newtonsoft.Json.JsonReader reader, System.Type o
}
}

if (typeof(SharedObject).IsAssignableFrom(objectType) && WritingTopLevelSharedObject(reader.Path))
{
var ident = (SharedObject)obj;
if (!SharedObjects.ContainsKey(ident.Id))
{
SharedObjects.Add(ident.Id, ident);
}
}

return obj;
}
catch (Exception ex)
Expand Down

0 comments on commit 73ed8d6

Please sign in to comment.