< Summary

Information
Class: Spdx3.Serialization.SpdxWrapperConverter<T>
Assembly: Spdx3
File(s): /home/runner/work/Spdx3/Spdx3/Spdx3/Serialization/SpdxWrapperConverter.cs
Line coverage
92%
Covered lines: 157
Uncovered lines: 12
Coverable lines: 169
Total lines: 464
Line coverage: 92.8%
Branch coverage
81%
Covered branches: 123
Total branches: 151
Branch coverage: 81.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
GetGenericPropertyForObjectFromHashtable(...)88.46%262692.85%
GetJsonElementNameFromPropertyAttribute(...)87.5%88100%
GetPlaceHolderForProperty(...)71.42%1414100%
GetPropertyFromJsonElementName(...)100%1414100%
NewPlaceHolderObjectWithId(...)75%4485.71%
NormalizeKey(...)100%44100%
SetPropertyValue(...)83.33%6692.85%
Read(...)65.51%302988.88%
Write(...)88.88%1818100%
GetObjectFromHashTable()86.36%222293.33%
SetHashtableValue(...)50%7666.66%

File(s)

/home/runner/work/Spdx3/Spdx3/Spdx3/Serialization/SpdxWrapperConverter.cs

#LineLine coverage
 1using System.Collections;
 2using System.Reflection;
 3using System.Text.Json;
 4using System.Text.Json.Serialization;
 5using System.Text.RegularExpressions;
 6using Spdx3.Exceptions;
 7using Spdx3.Model;
 8using Spdx3.Model.Core.Classes;
 9using Spdx3.Utility;
 10
 11namespace Spdx3.Serialization;
 12
 13/// <summary>
 14/// This is the JsonConvertoo for the SpdxWrapper object
 15/// </summary>
 16/// <typeparam name="T"></typeparam>
 17internal class SpdxWrapperConverter<T> : JsonConverter<T>
 18{
 19    // As we read the object properties, first we read the name, then we read the value.  This is the name and
 20    // the key into the hashtable that we're about to set.
 1121    private string _currentHashTableKey = string.Empty;
 22
 23    // This array is for when the value of a hashTable entry is an array of values
 24    private List<object>? _currentValueArray;
 25
 26    // We keep a hashtable of values of objects in the @graph array as we read them, and then turn each
 27    // hashtable into an SpdxBaseClass object from the model
 1128    private Dictionary<string, object> _hashTable = new();
 29
 30    // Are we already working on a hashtable?  Because we don't process nested objects
 31    private bool _hashtableInProgress;
 32
 33    private static void GetGenericPropertyForObjectFromHashtable(PropertyInfo property, BaseModelClass result,
 34        KeyValuePair<string, object> entry)
 35    {
 3436        var propType = property.PropertyType;
 3437        var propIsListOfModelClasses = propType.GetGenericTypeDefinition() == typeof(IList<>) &&
 3438                                       propType.GetGenericArguments()[0].IsAssignableTo(typeof(BaseModelClass));
 3439        var propIsListOfEnums = propType.GetGenericTypeDefinition() == typeof(IList<>) &&
 3440                                propType.GetGenericArguments()[0].IsEnum;
 3441        var propIsNullableEnum = propType.GetGenericTypeDefinition() == typeof(Nullable<>) &&
 3442                                 propType.GetGenericArguments()[0].IsEnum;
 43
 3444        if (propIsListOfModelClasses)
 45        {
 46            // The property is a list of classes, but the Json just has a list of references.
 47            // We need to create placeholder objects from the ID's that we will swap out later
 2548            var l = property.GetValue(result);
 49
 2550            if (l is not IList propertyValueListOfObjects)
 51            {
 052                throw new Spdx3SerializationException($"Could not get list of objects for type {propType}");
 53            }
 54
 55            List<object> listOfIds;
 56
 2557            if (entry.Value is string)
 58            {
 1959                listOfIds = [];
 60            }
 61            else
 62            {
 663                listOfIds = entry.Value as List<object> ??
 664                            throw new Spdx3SerializationException("List of ID's is null");
 65            }
 66
 7767            foreach (var placeholder in listOfIds.Select(id => GetPlaceHolderForProperty(property, (string)id)))
 68            {
 969                propertyValueListOfObjects.Add(placeholder);
 70            }
 71        }
 972        else if (propIsListOfEnums)
 73        {
 274            var enumType = propType.GetGenericArguments()[0];
 75
 276            if (property.GetValue(result) is not IList listOfEnums)
 77            {
 078                throw new Spdx3SerializationException($"Could not get value of type {propType} as a list");
 79            }
 80
 1281            foreach (var id in (IList<object>)entry.Value)
 82            {
 483                var value = Enum.Parse(enumType, (string)id);
 484                listOfEnums.Add(value);
 85            }
 86        }
 787        else if (propIsNullableEnum)
 88        {
 689            var value = Enum.Parse(propType.GetGenericArguments()[0], (string)entry.Value);
 690            property.SetValue(result, value);
 91        }
 3492    }
 93
 94    /// <summary>
 95    ///     Given a property on a Model class, get its json element name (i.e., the value in the JsonPropertyName attrib
 96    /// </summary>
 97    /// <param name="prop">The property of an object</param>
 98    /// <returns>The string value of the JsonPropertyName for that property</returns>
 99    private static string GetJsonElementNameFromPropertyAttribute(PropertyInfo prop)
 100    {
 16101        var jsonPropertyAttribute = prop.GetCustomAttributes().FirstOrDefault(a => a is JsonPropertyNameAttribute);
 8102        return jsonPropertyAttribute != null ? (string)(jsonPropertyAttribute as dynamic).Name : string.Empty;
 103    }
 104
 105    /// <summary>
 106    ///     Create a placeholder object for the current property, with a specific ID - we'll replace the object later
 107    /// </summary>
 108    /// <param name="property">The property (of some other object) that we need a placeholder for</param>
 109    /// <param name="spdxId">The ID to give that placeholder so we can find its replacement later</param>
 110    /// <returns>A minimally populated instance of a class that is the type the property needs, with the ID set</returns
 111    /// <exception cref="Spdx3SerializationException">Any of a variety of things went wrong</exception>
 112    private static BaseModelClass GetPlaceHolderForProperty(PropertyInfo property, string spdxId)
 113    {
 66114        var propType = property.PropertyType ??
 66115                       throw new Spdx3SerializationException($"Could not determine type of property {property}");
 116
 66117        if (propType.IsGenericType && propType.GetGenericTypeDefinition() == typeof(IList<>))
 118        {
 9119            propType = propType.GetGenericArguments()[0] ??
 9120                       throw new Spdx3SerializationException(
 9121                           $"Could not determine generic argument of type {propType}");
 122        }
 123
 66124        if (propType.IsAbstract)
 125        {
 19126            var placeHolderClassName =
 19127                Regex.Replace(
 19128                    propType.FullName ??
 19129                    throw new Spdx3SerializationException($"Could not determine full name of property type {propType}"),
 19130                    @"\.Classes\.", ".Classes.Placeholder");
 19131            propType = Type.GetType(placeHolderClassName) ??
 19132                       throw new Spdx3SerializationException($"Could not get type {placeHolderClassName}");
 133        }
 134
 66135        var placeHolder = NewPlaceHolderObjectWithId(propType, spdxId);
 66136        return placeHolder;
 137    }
 138
 139    /// <summary>
 140    ///     Given a property name from a JSON element, derive the Object property it represents (along with all its type
 141    ///     info).
 142    ///     These correspond to the JsonPropertyNames of the properties in the Model classes.
 143    ///     For example, if the current object is an Annotation, and the json element name is "annotationType", this
 144    ///     corresponds to the AnnotationType property on the Annotation object (which is what is returned), and it is o
 145    ///     AnnotationType (the enum).
 146    /// </summary>
 147    /// <param name="typeToConvert">The object that has the property</param>
 148    /// <param name="elementName">The name of the property as found in the JSON file (e.g., "build_buildId")</param>
 149    /// <returns>The property info, or null if no match</returns>
 150    private static PropertyInfo GetPropertyFromJsonElementName(Type typeToConvert, string elementName)
 151    {
 272152        var eName = Regex.Replace(elementName, "^spdx:.*/", "");
 153
 5575154        foreach (var prop in typeToConvert.GetProperties())
 155        {
 6523156            var jsonPropertyAttribute = prop.GetCustomAttributes().FirstOrDefault(a => a is JsonPropertyNameAttribute);
 157
 2651158            if (jsonPropertyAttribute == null)
 159            {
 160                // This property didn't have
 161                continue;
 162            }
 163
 2650164            if (eName == (jsonPropertyAttribute as dynamic).Name)
 165            {
 271166                return prop;
 167            }
 168        }
 169
 1170        throw new Spdx3SerializationException(
 1171            $"Could not find a property with JsonPropertyNameAttribute matching {eName} on {typeToConvert.Name}");
 172    }
 173
 174
 175    /// <summary>
 176    ///     Factory method to return a placeholder of a specific type, with the required ID.
 177    ///     This method differs from using a constructor in that it does not require all the fields required to pass
 178    ///     validation, and it also does not add the new item to the Catalog.
 179    /// </summary>
 180    /// <param name="propType">The type of the placeholder needed</param>
 181    /// <param name="id">The ID of the placeholder</param>
 182    /// <returns>A new placeholder object of the specified type.</returns>
 183    /// <exception cref="Spdx3Exception"></exception>
 184    private static BaseModelClass NewPlaceHolderObjectWithId(Type propType, string id)
 185    {
 66186        if (Activator.CreateInstance(propType, true) is not BaseModelClass placeHolder)
 187        {
 0188            throw new Spdx3Exception($"Could not create placeholder for {propType.FullName}");
 189        }
 190
 66191        placeHolder.Type = Naming.SpdxTypeForClass(propType);
 66192        placeHolder.SpdxId = new Uri(id);
 193
 66194        if (placeHolder is Element element)
 195        {
 26196            element.Comment = "***Placeholder***";
 197        }
 198
 66199        return placeHolder;
 200    }
 201
 202    private static string NormalizeKey(string originalKey)
 203    {
 272204        var key = Regex.Replace(originalKey, "^spdx:.*/", "");
 272205        key = key == "@id" ? "spdxId" : key;
 272206        key = key == "@type" ? "type" : key;
 272207        return key;
 208    }
 209
 210    private static void SetPropertyValue(PropertyInfo property, object obj, object hashTableValue)
 211    {
 160212        if (property.DeclaringType is null)
 213        {
 0214            throw new Spdx3SerializationException($"Could not determine declaring type of property {property.Name}");
 215        }
 216
 160217        if (hashTableValue is double && property.PropertyType == typeof(int))
 218        {
 2219            property.SetValue(obj, Convert.ToInt32(hashTableValue));
 2220            return;
 221        }
 222
 223        try
 224        {
 158225            property.SetValue(obj, Convert.ChangeType(hashTableValue, property.PropertyType));
 157226        }
 1227        catch (Exception e)
 228        {
 1229            var ht = hashTableValue.GetType().FullName;
 1230            var pt = property.PropertyType.FullName;
 1231            throw new Spdx3SerializationException(
 1232                $"Property {property.Name} on {property.DeclaringType.FullName} is a {pt} but the value from the JSON wa
 1233                e);
 234        }
 157235    }
 236
 237    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 238    {
 7239        var result = new SpdxWrapper();
 240
 732241        while (reader.Read())
 242        {
 243            // The outer layers are not of interest, so just keep going until we get deeper in the structure
 728244            if (reader.CurrentDepth < 2)
 245            {
 246                continue;
 247            }
 248
 696249            switch (reader.TokenType)
 250            {
 251                case JsonTokenType.PropertyName:
 276252                    _currentHashTableKey = reader.GetString() ??
 276253                                           throw new Spdx3SerializationException("Property name was null");
 276254                    break;
 255
 256                case JsonTokenType.String:
 277257                    var strVal = reader.GetString() ?? throw new Spdx3SerializationException("String value is null");
 258
 277259                    if (_currentValueArray == null)
 260                    {
 261                        // Not in an array, so the string value goes directly into the property
 262262                        SetHashtableValue(strVal);
 263                    }
 264                    else
 265                    {
 266                        // In an array, so the string value goes into the array
 15267                        _currentValueArray.Add(strVal);
 268                    }
 269
 15270                    break;
 271
 272                case JsonTokenType.Number:
 273                    // Read all numbers as doubles. We will convert to integer when the property being assinged to is on
 3274                    var dblVal = reader.GetDouble();
 275
 3276                    if (_currentValueArray == null)
 277                    {
 278                        // Not in an array, so the int value goes directly into the property
 3279                        SetHashtableValue(dblVal);
 280                    }
 281                    else
 282                    {
 283                        // We're in an array, so the int value goes in the array
 0284                        _currentValueArray.Add(dblVal);
 285                    }
 286
 0287                    break;
 288
 289                case JsonTokenType.StartArray:
 290                    // If we already have an array in progress, we are nesting, and we can't do that (yet?)
 9291                    if (_currentValueArray != null)
 292                    {
 0293                        throw new Spdx3SerializationException("Can't process nested array values");
 294                    }
 295
 296                    // Start the new array
 9297                    _currentValueArray = [];
 9298                    break;
 299
 300                case JsonTokenType.EndArray:
 301                    // The array is over, set the hashtable property to the array value
 9302                    SetHashtableValue(_currentValueArray);
 9303                    _currentValueArray = null; // Get rid of the array buffer
 9304                    break;
 305
 306                case JsonTokenType.StartObject:
 307                    // There shouldn't be a hashtable already in progress at the start of a new object
 63308                    if (_hashtableInProgress)
 309                    {
 1310                        throw new Spdx3SerializationException("Can't process nested object values");
 311                    }
 312
 313                    // Start a new hashtable for holding values -- we'll make a model object out of it when we reach the
 62314                    _hashTable = new Dictionary<string, object>();
 62315                    _hashtableInProgress = true;
 62316                    break;
 317
 318                case JsonTokenType.EndObject:
 319                    // Turn the hashtable into Spdx class
 59320                    var cls = GetObjectFromHashTable();
 57321                    result.Graph.Add(cls);
 57322                    _hashTable.Clear(); // Get rid of the hashTable contents so we can safely start a new one
 57323                    _hashtableInProgress = false;
 57324                    break;
 325
 326                case JsonTokenType.None:
 327                case JsonTokenType.Comment:
 328                case JsonTokenType.True:
 329                case JsonTokenType.False:
 330                case JsonTokenType.Null:
 331                default:
 0332                    throw new Spdx3SerializationException($"Unexpected token type {reader.TokenType}");
 333            }
 334        }
 335
 2336        return (T)Convert.ChangeType(result, typeToConvert);
 337    }
 338
 339    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
 340    {
 4341        var props = value?.GetType().GetProperties() ??
 4342                    throw new Spdx3SerializationException("Could not get properties of value to write");
 343
 4344        writer.WriteStartObject();
 345
 32346        foreach (var prop in props.Where(prop => prop.GetValue(value) != null))
 347        {
 8348            var jsonElementName = GetJsonElementNameFromPropertyAttribute(prop);
 349
 8350            var propVal = prop.GetValue(value);
 351
 352            switch (propVal)
 353            {
 354                // If it's a list of OTHER SpdxClasses, don't serialize the objects, just serialize an array of referenc
 355                case IList spdxClasses:
 356                {
 4357                    writer.WritePropertyName(jsonElementName);
 4358                    writer.WriteStartArray();
 359
 42360                    foreach (var spdxClass in spdxClasses)
 361                    {
 17362                        if (spdxClass != null)
 363                        {
 17364                            JsonSerializer.Serialize(writer, spdxClass, options);
 365                        }
 366                    }
 4367                    writer.WriteEndArray();
 368
 4369                    continue;
 370                }
 371                case string:
 4372                    writer.WriteString(jsonElementName, propVal.ToString());
 373                    break;
 374            }
 375        }
 376
 4377        writer.WriteEndObject();
 4378    }
 379
 380    /// <summary>
 381    /// From the hashtable of values in the JSON, construct a model object with those values (for primitives) or placeho
 382    /// </summary>
 383    /// <returns>A partially constructed object from the hashtable values</returns>
 384    /// <exception cref="Spdx3SerializationException"></exception>
 385    /// <exception cref="Spdx3Exception"></exception>
 386    private BaseModelClass GetObjectFromHashTable()
 387    {
 388        // Get the type of object to create
 59389        var classTypeName = Naming.ClassNameForSpdxType(_hashTable);
 59390        var classType = Type.GetType(classTypeName) ??
 59391                        throw new Spdx3SerializationException($"Could not get type {classTypeName}");
 392
 59393        var result = Activator.CreateInstance(classType, true) as BaseModelClass;
 394
 59395        if (result == null)
 396        {
 0397            throw new Spdx3SerializationException($"Could not create an instance of type {classType}");
 398        }
 399
 400        // Populate the object with values
 660401        foreach (var entry in _hashTable)
 402        {
 272403            var key = NormalizeKey(entry.Key);
 404
 272405            var property = GetPropertyFromJsonElementName(classType, key);
 271406            var propType = property.PropertyType;
 407
 271408            if (propType.IsAssignableTo(typeof(BaseModelClass)))
 409            {
 57410                var placeHolder = GetPlaceHolderForProperty(property, (string)entry.Value);
 57411                property.SetValue(result, placeHolder);
 412            }
 214413            else if (propType == typeof(int))
 414            {
 2415                SetPropertyValue(property, result, entry.Value);
 416            }
 212417            else if (propType == typeof(double))
 418            {
 1419                SetPropertyValue(property, result, entry.Value);
 420            }
 211421            else if (propType == typeof(string))
 422            {
 91423                SetPropertyValue(property, result, entry.Value);
 424            }
 120425            else if (propType == typeof(Uri))
 426            {
 61427                SetPropertyValue(property, result, new Uri((string)entry.Value));
 428            }
 59429            else if (propType == typeof(DateTimeOffset))
 430            {
 5431                SetPropertyValue(property, result, DateTimeOffset.Parse((string)entry.Value));
 432            }
 54433            else if (propType.IsGenericType)
 434            {
 34435                GetGenericPropertyForObjectFromHashtable(property, result, entry);
 436            }
 20437            else if (propType.IsEnum)
 438            {
 20439                var value = Enum.Parse(propType, (string)entry.Value);
 20440                property.SetValue(result, value);
 441            }
 442            else
 443            {
 0444                throw new Spdx3SerializationException("No handler for property");
 445            }
 446        }
 447
 57448        return result;
 449    }
 450
 451    private void SetHashtableValue(object? value)
 452    {
 274453        if (_hashTable == null)
 454        {
 0455            throw new Spdx3SerializationException("Hash table is null");
 456        }
 457
 274458        if (_currentHashTableKey == null)
 459        {
 0460            throw new Spdx3SerializationException("Current hash table key is null");
 461        }
 274462        _hashTable[_currentHashTableKey] = value ?? throw new Spdx3SerializationException("Value is null");
 274463    }
 464}