File: Settings\TemplateCache.cs
Web Access
Project: src\src\sdk\src\TemplateEngine\Microsoft.TemplateEngine.Edge\Microsoft.TemplateEngine.Edge.csproj (Microsoft.TemplateEngine.Edge)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using System.Text;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.TemplateEngine.Abstractions;
using Microsoft.TemplateEngine.Abstractions.Mount;
using Microsoft.TemplateEngine.Abstractions.TemplatePackage;
using Microsoft.TemplateEngine.Utils;

namespace Microsoft.TemplateEngine.Edge.Settings
{
    internal class TemplateCache
    {
        private const string BulletSymbol = "\u2022";
        private static readonly Guid RunnableProjectGeneratorId = new("0C434DF7-E2CB-4DEE-B216-D7C58C8EB4B3");

        public TemplateCache(IReadOnlyList<ITemplatePackage> allTemplatePackages, ScanResult?[] scanResults, Dictionary<string, DateTime> mountPoints, IEngineEnvironmentSettings environmentSettings)
        {
            ILogger logger = environmentSettings.Host.Logger;

            // We need this dictionary to de-duplicate templates that have same identity
            // notice that IEnumerable<ScanResult> that we get in is order by priority which means
            // last template with same Identity will win, others will be ignored...
            var templateDeduplicationDictionary = new Dictionary<string, IList<(IScanTemplateInfo Template, ITemplatePackage TemplatePackage, ILocalizationLocator? Localization, IMountPoint MountPoint)>>();
            foreach (var scanResult in scanResults)
            {
                if (scanResult == null)
                {
                    continue;
                }

                foreach (IScanTemplateInfo template in scanResult.Templates)
                {
                    var templatePackage = allTemplatePackages.FirstOrDefault(tp => tp.MountPointUri == template.MountPointUri);

                    if (templateDeduplicationDictionary.ContainsKey(template.Identity))
                    {
                        templateDeduplicationDictionary[template.Identity].Add((template, templatePackage, GetBestLocalizationLocatorMatch(template), scanResult.MountPoint));
                    }
                    else
                    {
                        templateDeduplicationDictionary[template.Identity] = new List<(IScanTemplateInfo Template, ITemplatePackage TemplatePackage, ILocalizationLocator? Localization, IMountPoint)>
                        {
                            (template, templatePackage, GetBestLocalizationLocatorMatch(template), scanResult.MountPoint)
                        };
                    }
                }
            }

            var templates = new List<TemplateInfo>();
            foreach (var duplicatedIdentities in templateDeduplicationDictionary)
            {
                // last template with same Identity wins, others will be ignored due to applied deduplication logic
                (IScanTemplateInfo Template, ITemplatePackage TemplatePackage, ILocalizationLocator? Localization, IMountPoint MountPoint) chosenTemplate = duplicatedIdentities.Value.Last();

                ILocalizationLocator? loc = GetBestLocalizationLocatorMatch(chosenTemplate.Template);
                (string, JsonObject?)? hostFile = GetBestHostConfigMatch(chosenTemplate.Template, environmentSettings, chosenTemplate.MountPoint);

                templates.Add(new TemplateInfo(chosenTemplate.Template, loc, hostFile));
            }

            Version = Settings.TemplateInfo.CurrentVersion;
            Locale = CultureInfo.CurrentUICulture.Name;
            TemplateInfo = templates;
            MountPointsInfo = mountPoints;

            PrintOverlappingIdentityWarning(logger, templateDeduplicationDictionary);
        }

        public TemplateCache(JsonObject? contentJObject)
        {
            if (contentJObject != null && contentJObject.TryGetValueCaseInsensitive(nameof(Version), out JsonNode? versionToken))
            {
                Version = versionToken!.ToJsonString().Trim('"');
            }
            else
            {
                Version = null;
                TemplateInfo = [];
                MountPointsInfo = new Dictionary<string, DateTime>();
                Locale = string.Empty;
                return;
            }

            Locale = contentJObject.TryGetValueCaseInsensitive(nameof(Locale), out JsonNode? localeToken)
                ? localeToken!.GetValue<string>()
                : string.Empty;

            var mountPointInfo = new Dictionary<string, DateTime>();

            if (contentJObject.TryGetValueCaseInsensitive(nameof(MountPointsInfo), out JsonNode? mountPointInfoToken) && mountPointInfoToken is JsonObject mountPointInfoObj)
            {
                foreach (var entry in mountPointInfoObj)
                {
                    if (entry.Value != null)
                    {
                        mountPointInfo.Add(entry.Key, entry.Value.GetValue<DateTime>());
                    }
                }
            }

            MountPointsInfo = mountPointInfo;

            List<TemplateInfo> templateList = new List<TemplateInfo>();

            if (contentJObject.TryGetValueCaseInsensitive(nameof(TemplateInfo), out JsonNode? templateInfoToken) && templateInfoToken is JsonArray arr)
            {
                foreach (JsonNode? entry in arr)
                {
                    if (entry is JsonObject entryObj)
                    {
                        templateList.Add(Settings.TemplateInfo.FromJObject(entryObj));
                    }
                }
            }

            TemplateInfo = templateList;
        }

        [JsonPropertyName("Version")]
        public string? Version { get; }

        [JsonPropertyName("Locale")]
        public string Locale { get; }

        [JsonPropertyName("TemplateInfo")]
        public IReadOnlyList<TemplateInfo> TemplateInfo { get; }

        [JsonPropertyName("MountPointsInfo")]
        public Dictionary<string, DateTime> MountPointsInfo { get; }

        private ILocalizationLocator? GetBestLocalizationLocatorMatch(IScanTemplateInfo template)
        {
            if (template.Localizations is null)
            {
                return null;
            }

            if (!template.Localizations.Any())
            {
                return null;
            }

            string? bestMatch = GetBestLocaleMatch(template.Localizations.Keys);
            if (string.IsNullOrWhiteSpace(bestMatch))
            {
                return null;
            }
            return template.Localizations[bestMatch!];
        }

        /// <remarks>see https://source.dot.net/#System.Private.CoreLib/ResourceFallbackManager.cs.</remarks>
        private string? GetBestLocaleMatch(IEnumerable<string> availableLocalizations)
        {
            CultureInfo currentCulture = CultureInfo.CurrentUICulture;
            do
            {
                if (availableLocalizations.Contains(currentCulture.Name, StringComparer.OrdinalIgnoreCase))
                {
                    return currentCulture.Name;
                }
                currentCulture = currentCulture.Parent;
            }
            while (currentCulture.Name != CultureInfo.InvariantCulture.Name);
            return null;
        }

        // add warning for the case when there is an attempt to overwrite existing managed by new managed template
        private void PrintOverlappingIdentityWarning(ILogger logger, IDictionary<string, IList<(IScanTemplateInfo Template, ITemplatePackage TemplatePackage, ILocalizationLocator? Localization, IMountPoint)>> templateDeduplicationDictionary)
        {
            foreach (var identityToTemplates in templateDeduplicationDictionary)
            {
                // we print the message only if managed template wins and we have > 1 managed templates with overlapping identities
                var lastTemplate = identityToTemplates.Value.Last();
                var managedTemplates = identityToTemplates.Value.Where(templateInto => templateInto.TemplatePackage is IManagedTemplatePackage).ToArray();
                if (lastTemplate.TemplatePackage is IManagedTemplatePackage && managedTemplates.Length > 1)
                {
                    var templatesList = new StringBuilder();
                    foreach (var (templateName, packageId, _, _) in managedTemplates)
                    {
                        templatesList.AppendLine(string.Format(
                            LocalizableStrings.TemplatePackageManager_Warning_DetectedTemplatesIdentityConflict_Subentry,
                            BulletSymbol,
                            templateName.Name,
                            (packageId as IManagedTemplatePackage)?.DisplayName));
                    }

                    logger.LogWarning(string.Format(
                            LocalizableStrings.TemplatePackageManager_Warning_DetectedTemplatesIdentityConflict,
                            identityToTemplates.Key,
                            templatesList.ToString().TrimEnd(Environment.NewLine.ToCharArray()),
                            lastTemplate.Template.Name));
                }
            }
        }

        private (string, JsonObject?)? GetBestHostConfigMatch(IScanTemplateInfo newTemplate, IEngineEnvironmentSettings settings, IMountPoint mountPoint)
        {
            if (newTemplate.HostConfigFiles.TryGetValue(settings.Host.HostIdentifier, out string? preferredHostFilePath))
            {
                return (preferredHostFilePath, ReadHostFile(newTemplate, preferredHostFilePath, settings, mountPoint));
            }

            foreach (string fallbackHostName in settings.Host.FallbackHostTemplateConfigNames)
            {
                if (newTemplate.HostConfigFiles.TryGetValue(fallbackHostName, out string? fallbackHostFilePath))
                {
                    return (fallbackHostFilePath, ReadHostFile(newTemplate, fallbackHostFilePath, settings, mountPoint));
                }
            }
            return null;
        }

        private JsonObject? ReadHostFile(IScanTemplateInfo template, string path, IEngineEnvironmentSettings settings, IMountPoint mountPoint)
        {
            if (template.GeneratorId != RunnableProjectGeneratorId)
            {
                return null;
            }
            settings.Host.Logger.LogDebug($"Start loading host config {template.MountPointUri}{path}");
            try
            {
                IFile? hostFile = mountPoint.FileInfo(path);
                if (hostFile == null || !hostFile.Exists)
                {
                    throw new FileNotFoundException($"Host file '{hostFile?.GetDisplayPath()}' does not exist.");
                }
                return hostFile.ReadJObjectFromIFile();
            }
            catch (Exception e)
            {
                settings.Host.Logger.LogWarning(
                    e,
                    LocalizableStrings.TemplateInfo_Warning_FailedToReadHostData,
                    template.MountPointUri,
                    path);
            }
            finally
            {
                settings.Host.Logger.LogDebug($"End loading host config {template.MountPointUri}{path}");
            }
            return null;
        }

    }
}