| File: DirectoryBasedTemplate.cs | Web Access |
| Project: src\sdk\src\TemplateEngine\Microsoft.TemplateEngine.Orchestrator.RunnableProjects\Microsoft.TemplateEngine.Orchestrator.RunnableProjects.csproj (Microsoft.TemplateEngine.Orchestrator.RunnableProjects) |
// 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.Json.Nodes; using Microsoft.Extensions.Logging; using Microsoft.TemplateEngine.Abstractions; using Microsoft.TemplateEngine.Abstractions.Mount; using Microsoft.TemplateEngine.Abstractions.Parameters; using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Validation; using Microsoft.TemplateEngine.Utils; namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects { /// <summary> /// The class represents the template loaded from directory. /// The configuration can be loaded from file (production scenario) or loaded from <see cref="TemplateConfigModel"/> (test scenario). /// In both cases the template should be available from <see cref="TemplateSourceRoot"/>. /// </summary> internal abstract partial class DirectoryBasedTemplate { internal const string HostTemplateFileConfigBaseName = ".host.json"; internal const string LocalizationFilePrefix = "templatestrings."; internal const string LocalizationFileExtension = ".json"; protected const string AdditionalConfigFilesIndicator = "AdditionalConfigFiles"; private readonly List<IValidationEntry> _validationErrors = new(); /// <summary> /// Creates the instance of the class based on configuration from <paramref name="templateFile"/>. /// </summary> /// <exception cref="TemplateAuthoringException">when template configuration is invalid.</exception> /// <exception cref="InvalidOperationException">when template identity is null.</exception> /// <exception cref="NotSupportedException">when the template is not supported by current generator version.</exception> protected DirectoryBasedTemplate(IEngineEnvironmentSettings settings, IGenerator generator, IFile templateFile, string? baselineName = null) { EngineEnvironmentSettings = settings; //TODO: create specific logger if needed Logger = settings.Host.Logger; Generator = generator; ConfigFile = templateFile; if (ConfigFile.Parent?.Parent is null) { throw new TemplateAuthoringException(LocalizableStrings.Authoring_TemplateRootOutsideInstallSource); } ConfigDirectory = templateFile.Parent; TemplateSourceRoot = ConfigFile.Parent.Parent; ConfigurationModel = TemplateConfigModel.FromJObject( MergeAdditionalConfiguration(templateFile.ReadJObjectFromIFile(), templateFile), Logger, baselineName); CheckGeneratorVersionRequiredByTemplate(); } /// <summary> /// Test constructor. Do not use in production. /// This constructor does not set the location of configuration file. /// </summary> protected DirectoryBasedTemplate(IEngineEnvironmentSettings settings, IGenerator generator, TemplateConfigModel configModel, IDirectory templateSource) { EngineEnvironmentSettings = settings; //TODO: create specific logger if needed Logger = settings.Host.Logger; Generator = generator; TemplateSourceRoot = templateSource; ConfigurationModel = configModel; CheckGeneratorVersionRequiredByTemplate(); } public ILogger Logger { get; } /// <summary> /// Gets the configuration model. /// For test purposes, preloaded model can be used. /// </summary> public TemplateConfigModel ConfigurationModel { get; } /// <summary> /// Gets the directory with source files for the template. /// </summary> public IDirectory TemplateSourceRoot { get; } public IEngineEnvironmentSettings EngineEnvironmentSettings { get; } /// <summary> /// Gets configuration <see cref="IFile"></see>. <see langword="null"/> when the template is created from the model built from code. /// </summary> public virtual IFile? ConfigFile { get; } /// <summary> /// Gets the configuration directory. <see langword="null"/> when the template is created from the model built from code. /// </summary> public virtual IDirectory? ConfigDirectory { get; } protected IGenerator Generator { get; } /// <summary> /// Gets the template parameters. /// </summary> protected IParameterDefinitionSet Parameters => new ParameterDefinitionSet(ConfigurationModel.ExtractParameters()); internal Task ValidateAsync(ValidationScope scope, CancellationToken cancellationToken) { try { return ValidationManager.Instance.ValidateTemplateAsync(EngineEnvironmentSettings, this, scope, cancellationToken); } catch (Exception ex) when (ex is not TaskCanceledException) { //TODO: better error handling Logger.LogError("Failed to validate template: {ex}", ex.Message); } return Task.CompletedTask; } /// <summary> /// Parses host file name to get host identifier. /// </summary> /// <param name="filename"></param> /// <returns></returns> protected string ParseHostFileName(string filename) => filename.Replace(HostTemplateFileConfigBaseName, string.Empty); /// <summary> /// Parses localization file name to get locale. /// </summary> /// <param name="locFile">localization file.</param> /// <returns></returns> protected CultureInfo? ParseLocFileName(IFile locFile) { string filename = locFile.Name; string localeStr = filename.Substring(LocalizationFilePrefix.Length, filename.Length - LocalizationFilePrefix.Length - LocalizationFileExtension.Length); CultureInfo? locale = null; try { // PERF: Avoid calling CultureInfo.GetCultures and searching the results as it heavily allocates on each invocation. locale = CultureInfo.GetCultureInfo(localeStr); } catch (CultureNotFoundException) { Logger.LogWarning(LocalizableStrings.LocalizationModelDeserializer_Error_UnknownLocale, localeStr); } return locale; } /// <summary> /// Checks the <paramref name="primarySource"/> for additional configuration files. /// If found, merges them all together. /// Returns the merged JObject (or the original if there was nothing to merge). /// Additional files must be in the same folder as the template file. /// </summary> /// <exception cref="TemplateAuthoringException">when additional files configuration is invalid.</exception> private static JsonObject MergeAdditionalConfiguration(JsonObject primarySource, IFileSystemInfo primarySourceConfig) { IReadOnlyList<string> otherFiles = primarySource.ArrayAsStrings(AdditionalConfigFilesIndicator); if (!otherFiles.Any()) { return primarySource; } JsonObject combinedSource = primarySource.DeepCloneObject(); foreach (string partialConfigFileName in otherFiles) { if (!partialConfigFileName.EndsWith("." + RunnableProjectGenerator.TemplateConfigFileName)) { throw new TemplateAuthoringException(string.Format(LocalizableStrings.SimpleConfigModel_AuthoringException_MergeConfiguration_InvalidFileName, partialConfigFileName, RunnableProjectGenerator.TemplateConfigFileName), partialConfigFileName); } IFile? partialConfigFile = (primarySourceConfig.Parent?.EnumerateFiles(partialConfigFileName, SearchOption.TopDirectoryOnly).FirstOrDefault(x => string.Equals(x.Name, partialConfigFileName))) ?? throw new TemplateAuthoringException( string.Format( LocalizableStrings.SimpleConfigModel_AuthoringException_MergeConfiguration_FileNotFound, partialConfigFileName), partialConfigFileName); JsonObject partialConfigJson = partialConfigFile.ReadJObjectFromIFile(); combinedSource.Merge(partialConfigJson); } return combinedSource; } /// <summary> /// Checks if the template is supported by current generator version. /// </summary> /// <exception cref="NotSupportedException">when the template is not supported by current generator version.</exception> /// <exception cref="InvalidOperationException">when check for the version leads to unexpected result.</exception> private void CheckGeneratorVersionRequiredByTemplate() { if (string.IsNullOrWhiteSpace(ConfigurationModel.GeneratorVersions)) { return; } string allowedGeneratorVersions = ConfigurationModel.GeneratorVersions!; if (!VersionStringHelpers.TryParseVersionSpecification(allowedGeneratorVersions, out IVersionSpecification? versionChecker)) { throw new NotSupportedException(string.Format(LocalizableStrings.RunnableProjectGenerator_Exception_TemplateVersionNotSupported, allowedGeneratorVersions, RunnableProjectGenerator.GeneratorVersion)); } if (versionChecker is null) { throw new InvalidOperationException($"{nameof(versionChecker)} cannot be null when {nameof(VersionStringHelpers.TryParseVersionSpecification)} is 'true'"); } if (!versionChecker.CheckIfVersionIsValid(RunnableProjectGenerator.GeneratorVersion)) { throw new NotSupportedException(string.Format(LocalizableStrings.RunnableProjectGenerator_Exception_TemplateVersionNotSupported, allowedGeneratorVersions, RunnableProjectGenerator.GeneratorVersion)); } } } }