File: PackageIndex.cs
Web Access
Project: src\src\Microsoft.DotNet.Build.Tasks.Packaging\src\Microsoft.DotNet.Build.Tasks.Packaging.csproj (Microsoft.DotNet.Build.Tasks.Packaging)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using NuGet.Frameworks;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Collections;
using Newtonsoft.Json.Linq;
using System.Xml.Linq;
 
namespace Microsoft.DotNet.Build.Tasks.Packaging
{
    public class PackageIndex
    {
        static JsonSerializer s_serializer = CreateSerializer();
 
        static ConcurrentDictionary<string, PackageIndex> s_indexCache = new ConcurrentDictionary<string, PackageIndex>();
 
        public SortedDictionary<string, PackageInfo> Packages { get; set; } = new SortedDictionary<string, PackageInfo>();
 
        public SortedDictionary<string, string> ModulesToPackages { get; set; } = new SortedDictionary<string, string>();
 
        public MetaPackages MetaPackages { get; set; } = new MetaPackages();
 
        public string PreRelease { get; set; }
 
        [JsonIgnore]
        public HashSet<string> IndexSources { get; set; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 
        public static PackageIndex Load(IEnumerable<string> packageIndexFiles)
        {
            string indexKey = String.Join("|",
                packageIndexFiles.Select(packageIndexFile => new FileInfo(packageIndexFile))
                                 .Select(packageIndexFileInfo => $"{packageIndexFileInfo.FullName}:{packageIndexFileInfo.Length}:{packageIndexFileInfo.LastWriteTimeUtc.Ticks}"));
 
            PackageIndex result = null;
 
            if (s_indexCache.TryGetValue(indexKey, out result))
            {
                return result;
            }
 
            foreach(var packageIndexFile in packageIndexFiles)
            {
                if (result == null)
                {
                    result = Load(packageIndexFile);
                }
                else
                {
                    result.Merge(packageIndexFile);
                }
            }
 
            s_indexCache[indexKey] = result;
 
            return result;
        }
 
        public static PackageIndex Load(string packageIndexFile)
        {
            using (var file = File.OpenText(packageIndexFile))
            using (var jsonTextReader = new JsonTextReader(file))
            {
                var result = s_serializer.Deserialize<PackageIndex>(jsonTextReader);
                result.IndexSources.Add(Path.GetFullPath(packageIndexFile));
                return result;
            }
        }
 
        public void Save(string path)
        {
            string directory = Path.GetDirectoryName(path);
            if (!String.IsNullOrEmpty(directory) && !Directory.Exists(directory))
            {
                Directory.CreateDirectory(directory);
            }
 
            using (var file = File.CreateText(path))
            {
                s_serializer.Serialize(file, this);
            }
        }
 
        private static JsonSerializer CreateSerializer()
        {
            var serializer = new JsonSerializer();
            serializer.StringEscapeHandling = StringEscapeHandling.EscapeNonAscii;
            serializer.Formatting = Formatting.Indented;
            serializer.NullValueHandling = NullValueHandling.Ignore;
            serializer.DefaultValueHandling = DefaultValueHandling.Ignore;
 
            serializer.Converters.Add(new VersionConverter());
            serializer.Converters.Add(new InboxFrameworksConverter());
            serializer.Converters.Add(new MetaPackagesConverter());
 
            return serializer;
        }
 
        /// <summary>
        /// Merges an index into the currently loaded index if not already merged.
        /// </summary>
        /// <param name="otherIndexFile"></param>
        public void Merge(string otherIndexFile)
        {
            if (!IndexSources.Contains(otherIndexFile))
            {
                Merge(Load(otherIndexFile));
            }
        }
 
        /// <summary>
        /// Merges a list of indexes into the currently loaded index if not already merged.
        /// </summary>
        /// <param name="otherIndexFiles"></param>
        public void Merge(IEnumerable<string> otherIndexFiles)
        {
            foreach(var otherIndexFile in otherIndexFiles)
            {
                Merge(otherIndexFile);
            }
        }
 
        /// <summary>
        /// Merges another packageIndex into this package index.  For any overlapping
        /// data 'other' has precedence.
        /// </summary>
        /// <param name="other"></param>
        public void Merge(PackageIndex other)
        {
            if (other.IndexSources.IsSubsetOf(IndexSources))
            {
                return;
            }
 
            // if pre-release is set on this index and different than the other
            // move pre-release to individual infos
            if (PreRelease != null && !PreRelease.Equals(other.PreRelease))
            {
                foreach(var info in Packages.Values)
                {
                    if (info.PreRelease == null)
                    {
                        info.PreRelease = PreRelease;
                    }
                }
 
                PreRelease = null;
            }
 
            foreach(var otherPackage in other.Packages)
            {
                var otherInfo = otherPackage.Value;
                PackageInfo info;
 
                if (Packages.TryGetValue(otherPackage.Key, out info))
                {
                    info.Merge(otherInfo);
                }
                else
                {
                    Packages[otherPackage.Key] = info = otherInfo;
 
                    // if pre-release is set on the other index and doesn't match the value of the info, set it
                    if (other.PreRelease != null && !other.PreRelease.Equals(info.PreRelease))
                    {
                        info.PreRelease = other.PreRelease;
                    }
                }
            }
 
            foreach(var otherModuleToPackage in other.ModulesToPackages)
            {
                ModulesToPackages[otherModuleToPackage.Key] = otherModuleToPackage.Value;
            }
 
            MetaPackages.Merge(other.MetaPackages);
 
            foreach(var otherIndexSource in other.IndexSources)
            {
                IndexSources.Add(otherIndexSource);
            }
        }
 
        public void MergeInboxFromLayout(NuGetFramework framework, string layoutDirectory, bool addPackages = true)
        {
            foreach(var file in Directory.EnumerateFiles(layoutDirectory, "*.dll", SearchOption.AllDirectories))
            {
                var assemblyName = Path.GetFileNameWithoutExtension(file);
                var assemblyVersion = VersionUtility.GetAssemblyVersion(file);
 
                TryAddInbox(assemblyName, framework, assemblyVersion, addPackages);
            }
        }
 
        public void MergeFrameworkLists(string frameworkListDirectory, bool addPackages = true)
        {
            foreach (string frameworkDir in Directory.EnumerateDirectories(frameworkListDirectory))
            {
                string targetFrameworkMoniker = Path.GetFileName(frameworkDir);
                NuGetFramework framework = NuGetFramework.Parse(targetFrameworkMoniker);
                foreach (string frameworkListPath in Directory.EnumerateFiles(frameworkDir, "*.xml"))
                {
                    MergeFrameworkList(framework, frameworkListPath, addPackages);
                }
            }
        }
 
        public void MergeFrameworkList(NuGetFramework framework, string frameworkListPath, bool addPackages = true)
        {
            XDocument frameworkList = XDocument.Load(frameworkListPath);
            foreach (var file in frameworkList.Element("FileList").Elements("File"))
            {
                string assemblyName = file.Attribute("AssemblyName").Value;
                var versionAttribute = file.Attribute("Version");
                Version supportedVersion = (versionAttribute != null) ? new Version(versionAttribute.Value) : VersionUtility.MaxVersion;
 
                TryAddInbox(assemblyName, framework, supportedVersion, addPackages);
            }
        }
 
        private void TryAddInbox(string assemblyName, NuGetFramework framework, Version version, bool addPackages = true)
        {
            PackageInfo info;
            if (Packages.TryGetValue(assemblyName, out info))
            {
                info.InboxOn.AddInboxVersion(framework, version);
            }
            else if (addPackages)
            {
                Packages[assemblyName] = info = new PackageInfo();
                info.InboxOn.AddInboxVersion(framework, version);
            }
        }
 
        // helper functions
        public bool TryGetBaseLineVersion(string packageId, out Version baseLineVersion)
        {
            PackageInfo info;
            baseLineVersion = null;
 
            if (Packages.TryGetValue(packageId, out info))
            {
                baseLineVersion = info.BaselineVersion;
            }
 
            return baseLineVersion != null;
        }
 
        public IEnumerable<NuGetFramework> GetAlllInboxFrameworks()
        {
            // very inefficient, included for legacy reasons.
            return Packages.Values.SelectMany(info => info.InboxOn.GetInboxFrameworks()).Distinct().ToArray();
        }
 
        public IEnumerable<NuGetFramework> GetInboxFrameworks(string assemblyName)
        {
            IEnumerable<NuGetFramework> inboxFrameworks = null;
            PackageInfo info;
 
            if (Packages.TryGetValue(assemblyName, out info))
            {
                inboxFrameworks = info.InboxOn.GetInboxFrameworks();
            }
 
            return inboxFrameworks;
        }
 
        public IEnumerable<NuGetFramework> GetInboxFrameworks(string assemblyName, string assemblyVersionString)
        {
            Version assemblyVersion = FrameworkUtilities.Ensure4PartVersion(assemblyVersionString);
 
            return GetInboxFrameworks(assemblyName, assemblyVersion);
        }
 
        public IEnumerable<NuGetFramework> GetInboxFrameworks(string assemblyName, Version assemblyVersion)
        {
            IEnumerable<NuGetFramework> inboxFrameworks = null;
            PackageInfo info;
 
            if (Packages.TryGetValue(assemblyName, out info))
            {
                inboxFrameworks = info.InboxOn.GetInboxVersions().Where(p => p.Value >= assemblyVersion).Select(p => p.Key);
            }
 
            return inboxFrameworks;
        }
 
        public bool IsInbox(string assemblyName, NuGetFramework framework, string assemblyVersionString)
        {
            Version assemblyVersion = FrameworkUtilities.Ensure4PartVersion(assemblyVersionString);
 
            return IsInbox(assemblyName, framework, assemblyVersion);
        }
 
        public bool IsInbox(string assemblyName, NuGetFramework framework, Version assemblyVersion)
        {
            PackageInfo info;
            bool isInbox = false;
 
            if (Packages.TryGetValue(assemblyName, out info))
            {
                isInbox = info.InboxOn.IsInbox(framework, assemblyVersion);
            }
 
            return isInbox;
        }
 
        public bool IsStable(string packageId, Version packageVersion)
        {
            PackageInfo info;
            bool isStable = false;
 
            if (Packages.TryGetValue(packageId, out info))
            {
                isStable = info.StableVersions.Contains(packageVersion);
            }
 
            return isStable;
        }
 
        public string GetPreRelease(string packageId)
        {
            PackageInfo info;
            string preRelease = null;
 
            if (Packages.TryGetValue(packageId, out info))
            {
                preRelease = info.PreRelease;
            }
 
            if (preRelease == null)
            {
                preRelease = PreRelease;
            }
 
            return preRelease;
        }
 
        public Version GetPackageVersionForAssemblyVersion(string packageId, Version assemblyVersion)
        {
            PackageInfo info;
            Version packageVersion = null;
 
            if (assemblyVersion != null)
            {
                if (Packages.TryGetValue(packageId, out info))
                {
                    packageVersion = info.GetPackageVersionForAssemblyVersion(assemblyVersion);
                }
                else
                {
                    // if not found assume 1:1 with assembly version
                    packageVersion = VersionUtility.As3PartVersion(assemblyVersion);
                }
            }
 
            return packageVersion;
        }
    }
 
    public class PackageInfo
    {
        public HashSet<Version> StableVersions { get; set; } = new HashSet<Version>();
 
        public bool ShouldSerializeStableVersions() { return StableVersions.Count > 0; }
 
        /// <summary>
        /// Minimum version to use when referencing this package.
        /// </summary>
        public Version BaselineVersion { get; set; }
 
        /// <summary>
        /// Mapping of frameworks which contain this package inbox (or in a framework package)
        /// </summary>
        public InboxFrameworks InboxOn { get; set; } = new InboxFrameworks();
 
        public bool ShouldSerializeInboxFrameworkAssemblyVersions() { return InboxOn.Count > 0; }
 
        /// <summary>
        /// Mapping of assembly version to package version which (first) contains that assembly version.
        /// </summary>
        public SortedDictionary<Version, Version> AssemblyVersionInPackageVersion { get; set; } = new SortedDictionary<Version, Version>();
        
        public bool ShouldSerializeAssemblyVersionInPackageVersion() { return AssemblyVersionInPackageVersion.Count > 0; }
 
        public string PreRelease { get; set; }
 
        public void Merge(PackageInfo other)
        {
            StableVersions.UnionWith(other.StableVersions);
 
            if (other.BaselineVersion != null && BaselineVersion == null)
            {
                BaselineVersion = other.BaselineVersion;
            }
 
            if (other.PreRelease != null && PreRelease == null)
            {
                PreRelease = other.PreRelease;
            }
 
            foreach(var inboxOnPair in other.InboxOn.GetInboxVersions())
            {
                InboxOn.AddInboxVersion(inboxOnPair.Key, inboxOnPair.Value);
            }
 
            foreach (var assemblyVersionInPackage in other.AssemblyVersionInPackageVersion)
            {
                Version otherAssemblyVersion = assemblyVersionInPackage.Key;
                Version otherPackageVersion = assemblyVersionInPackage.Value;
 
                AddAssemblyVersionInPackage(otherAssemblyVersion, otherPackageVersion);
            }
        }
 
        public void AddAssemblyVersionsInPackage(IEnumerable<Version> assemblyVersions, Version packageVersion)
        {
            foreach (var assemblyVersion in assemblyVersions)
            {
                AddAssemblyVersionInPackage(assemblyVersion, packageVersion);
            }
        }
 
        public void AddAssemblyVersionInPackage(Version assemblyVersion, Version packageVersion)
        {
            Version existingPackageVersion;
            if (AssemblyVersionInPackageVersion.TryGetValue(assemblyVersion, out existingPackageVersion))
            {
                bool existingStable = StableVersions.Contains(existingPackageVersion);
                bool updateStable = StableVersions.Contains(packageVersion);
                
                // always prefer a stable package over unstable package
                // use the highest unstable package version
                // use the lowest stable package version
                if ((updateStable && !existingStable) || // update to stable from unstable
                    (updateStable && existingStable && packageVersion < existingPackageVersion) || // update to lower stable
                    (!updateStable && !existingStable && packageVersion > existingPackageVersion)) // update to higher non-stable version
                {
                    AssemblyVersionInPackageVersion[assemblyVersion] = packageVersion;
                }
            }
            else
            {
                AssemblyVersionInPackageVersion[assemblyVersion] = packageVersion;
            }
        }
        public Version GetPackageVersionForAssemblyVersion(NuGetFramework framework, Version assemblyVersion)
        {
            Version packageVersion = null;
 
            if (assemblyVersion != null)
            {
                // prefer an explicit mapping
                if (!AssemblyVersionInPackageVersion.TryGetValue(assemblyVersion, out packageVersion))
                {
                    // if not found assume 1:1 with assembly version
                    packageVersion = VersionUtility.As3PartVersion(assemblyVersion);
                }
            }
 
            return packageVersion;
        }
 
        public Version GetPackageVersionForAssemblyVersion(Version assemblyVersion)
        {
            Version packageVersion = null;
 
            if (assemblyVersion != null)
            {
                // prefer an explicit mapping
                if (!AssemblyVersionInPackageVersion.TryGetValue(assemblyVersion, out packageVersion))
                {
                    // if not found assume 1:1 with assembly version
                    packageVersion = VersionUtility.As3PartVersion(assemblyVersion);
                }
            }
 
            return packageVersion;
        }
    }
 
    public class InboxFrameworks
    {
        private SortedDictionary<string, SortedDictionary<Version, Version>> inboxVersions = new SortedDictionary<string, SortedDictionary<Version, Version>>();
 
        public int Count { get { return inboxVersions.Sum(m => m.Value.Count); } }
 
        public void AddInboxVersion(NuGetFramework framework, Version assemblyVersion)
        {
            var normalizedFramework = NormalizeFramework(framework);
            var frameworkKey = GetFrameworkKey(normalizedFramework);
            var frameworkVersion = normalizedFramework.Version;
 
            SortedDictionary<Version, Version> mappings;
            if (!inboxVersions.TryGetValue(frameworkKey, out mappings))
            {
                inboxVersions[frameworkKey] = mappings = new SortedDictionary<Version, Version>();
            }
            
            if (assemblyVersion == null)
            {
                // explicitly not supported.
                mappings[normalizedFramework.Version] = assemblyVersion;
            }
            else
            {
                List<Version> redundantMappings = new List<Version>();
 
                var addMapping = true;
                foreach(var mapping in mappings)
                {
                    var existingFrameworkVersion = mapping.Key;
                    var existingAssemblyVersion = mapping.Value;
 
                    if (existingFrameworkVersion <= frameworkVersion)
                    {
                        if (existingAssemblyVersion != null && existingAssemblyVersion >= assemblyVersion)
                        {
                            // lower or same framework already maps this or higher version, don't add it
                            addMapping = false;
                        }
                    }
                    else
                    {
                        if (existingAssemblyVersion <= assemblyVersion)
                        {
                            // higher framework version with an equal or lower assembly version, remove it.
                            redundantMappings.Add(mapping.Key);
                        }
                    }
                }
 
                if (addMapping)
                {
                    mappings[normalizedFramework.Version] = assemblyVersion;
                }
 
                foreach(var redundantMapping in redundantMappings)
                {
                    mappings.Remove(redundantMapping);
                }
            }
 
 
        }
 
        public IEnumerable<NuGetFramework> GetInboxFrameworks()
        {
            foreach (var framework in inboxVersions)
            {
                foreach (var frameworkVersion in framework.Value.Keys)
                {
                    var fx = FromFrameworkKeyAndVersion(framework.Key, frameworkVersion);
 
                    yield return fx;
                }
            }
        }
 
        public IEnumerable<KeyValuePair<NuGetFramework, Version>> GetInboxVersions()
        {
            foreach (var framework in inboxVersions)
            {
                foreach (var frameworkInboxVersionPair in framework.Value)
                {
                    var frameworkVersion = frameworkInboxVersionPair.Key;
                    var assemblyVersion = frameworkInboxVersionPair.Value;
 
                    var fx = FromFrameworkKeyAndVersion(framework.Key, frameworkVersion);
 
                    yield return new KeyValuePair<NuGetFramework, Version>(fx, assemblyVersion);
                }
            }
        }
 
        public bool IsAnyVersionInbox(NuGetFramework framework)
        {
            var normalizedFramework = NormalizeFramework(framework);
            var key = GetFrameworkKey(normalizedFramework);
 
            SortedDictionary<Version, Version> mappings;
            if (!inboxVersions.TryGetValue(key, out mappings))
            {
                // no inbox info for this framework
                return false;
            }
 
            return mappings.Keys.Any(fxVer => fxVer <= framework.Version);
        }
 
        public bool IsInbox(NuGetFramework framework, Version assemblyVersion, bool permitRevisions = false)
        {
            var normalizedFramework = NormalizeFramework(framework);
            var key = GetFrameworkKey(normalizedFramework);
 
            SortedDictionary<Version, Version> mappings;
            if (!inboxVersions.TryGetValue(key, out mappings))
            {
                // no inbox info for this framework
                return false;
            }
 
            Version assemblyVersionInbox;
            if (mappings.TryGetValue(normalizedFramework.Version, out assemblyVersionInbox))
            {
                if (assemblyVersionInbox == null)
                {
                    // null entry means it's explicitly not inbox
                    return false;
                }
 
                if (assemblyVersionInbox == VersionUtility.MaxVersion)
                {
                    return true;
                }
 
                // inbox if explicit entry is greater than or equal to current
                return permitRevisions ? 
                    VersionUtility.IsCompatibleApiVersion(assemblyVersionInbox, assemblyVersion) : 
                    assemblyVersionInbox >= assemblyVersion;
            }
 
            // find nearest
            var compatibleMapping = mappings.LastOrDefault(m => m.Key < normalizedFramework.Version);
            if (compatibleMapping.Key == null || compatibleMapping.Value == null)
            {
                // either no compatible mapping, or compatible mapping explicitly not inbox
                return false;
            }
 
            if (compatibleMapping.Value == VersionUtility.MaxVersion)
            {
                return true;
            }
 
            // inbox if compatible entry is greater than or equal to current
            return permitRevisions ?
                    VersionUtility.IsCompatibleApiVersion(compatibleMapping.Value, assemblyVersion) :
                    compatibleMapping.Value >= assemblyVersion;
        }
 
 
        private static NuGetFramework NormalizeFramework(NuGetFramework framework)
        {
            if (framework == FrameworkConstants.CommonFrameworks.NetCore50)
            {
                // normalize netcore50 -> UAP10.
                // this permits us to model that netcore50/uap10 should not inherit inbox state from netcore4*
                return FrameworkConstants.CommonFrameworks.UAP10;
            }
            else if (framework == FrameworkConstants.CommonFrameworks.NetCore45)
            {
                return FrameworkConstants.CommonFrameworks.Win8;
            }
            else if (framework == FrameworkConstants.CommonFrameworks.NetCore451)
            {
                return FrameworkConstants.CommonFrameworks.Win81;
            }
 
            return framework;
        }
 
        private static string GetFrameworkKey(NuGetFramework framework)
        {
            if (!String.IsNullOrEmpty(framework.Profile))
            {
                return framework.Framework + "," + framework.Profile;
            }
            return framework.Framework;
        }
 
        private static NuGetFramework FromFrameworkKeyAndVersion(string key, Version version)
        {
            var parts = key.Split(',');
 
            if (parts.Length > 1)
            {
                return new NuGetFramework(parts[0], version, parts[1]);
            }
            else
            {
                return new NuGetFramework(key, version);
            }
        }
    }
 
    public class InboxFrameworksConverter : JsonConverter
    {
        private const string AnyVersion = "Any";
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(InboxFrameworks);
        }
 
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var jobj = JObject.Load(reader);
 
            var result = new InboxFrameworks();
            foreach (var property in jobj.Properties())
            {
                var versionString = property.Value.ToString();
                var version = versionString.Equals(AnyVersion, StringComparison.OrdinalIgnoreCase) ? VersionUtility.MaxVersion : new Version(versionString);
                result.AddInboxVersion(NuGetFramework.Parse(property.Name), version);
            }
 
            return result;
        }
 
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            if (value == null)
            {
                writer.WriteNull();
            }
            else if (value is InboxFrameworks)
            {
                writer.WriteStartObject();
 
                foreach(var frameworkPair in ((InboxFrameworks)value).GetInboxVersions())
                {
                    var shortName = frameworkPair.Key.GetShortFolderName();
                    var assemblyVersion = frameworkPair.Value;
                    var assemblyVersionString = assemblyVersion == VersionUtility.MaxVersion ? AnyVersion : assemblyVersion.ToString();
                    writer.WritePropertyName(shortName);
                    writer.WriteValue(assemblyVersionString);
                }
 
                writer.WriteEndObject();
            }
            else
            {
                throw new JsonSerializationException($"Expected {nameof(InboxFrameworks)} but got {value.GetType()}");
            }
        }
    }
 
    public class MetaPackages
    {
        private Dictionary<string, string> packageToMetaPackage = new Dictionary<string, string>();
 
        public void AddMetaPackageMapping(string packageId, string metaPackageId)
        {
            string existingMetaPackage;
 
            if (packageToMetaPackage.TryGetValue(packageId, out existingMetaPackage))
            {
                if (existingMetaPackage != metaPackageId)
                {
                    throw new InvalidOperationException($"Package {packageId} cannot be mapped to {metaPackageId} because it is already mapped to {existingMetaPackage}.");
                }
            }
            else
            {
                packageToMetaPackage.Add(packageId, metaPackageId);
            }
        }
 
        public string GetMetaPackageId(string packageId)
        {
            string metaPackageId;
 
            packageToMetaPackage.TryGetValue(packageId, out metaPackageId);
 
            return metaPackageId;
        }
 
        public void Merge(MetaPackages other)
        {
            foreach(var metaPackage in other.packageToMetaPackage)
            {
                // only merge a meta-package definition if it wasn't defined
                if (!packageToMetaPackage.ContainsKey(metaPackage.Key))
                {
                    packageToMetaPackage.Add(metaPackage.Key, metaPackage.Value);
                }
            }
        }
 
        internal IEnumerable<IGrouping<string, string>> GetMetaPackageGrouping()
        {
            return packageToMetaPackage.GroupBy(p => p.Value, p => p.Key);
        }
    }
 
    public class MetaPackagesConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(MetaPackages);
        }
 
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var jobj = JObject.Load(reader);
 
            var result = new MetaPackages();
            foreach (var property in jobj.Properties())
            {
                if (property.Value == null || property.Value.Type == JTokenType.Null)
                {
                    continue;
                }
 
                var metaPackageId = property.Name;
                
                var metaPackageArray = property.Value as JArray;
 
                if (metaPackageArray == null)
                {
                    throw new JsonSerializationException($"Expected array for property {metaPackageId}");
                }
 
                foreach(var package in metaPackageArray)
                {
                    result.AddMetaPackageMapping(package.ToString(), metaPackageId);
                }
            }
 
            return result;
        }
 
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            if (value == null)
            {
                writer.WriteNull();
            }
            else if (value is MetaPackages)
            {
                writer.WriteStartObject();
                foreach(var metaPackage in ((MetaPackages)value).GetMetaPackageGrouping())
                {
                    writer.WritePropertyName(metaPackage.Key);
                    writer.WriteStartArray();
                    foreach(var package in metaPackage)
                    {
                        writer.WriteValue(package);
                    }
                    writer.WriteEndArray();
                }
 
                writer.WriteEndObject();
            }
            else
            {
                throw new JsonSerializationException($"Expected {nameof(MetaPackages)} but got {value.GetType()}");
            }
        }
    }
 
}