using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using CTA.Rules.Config; using CTA.WebForms.Helpers.TagConversion; using CTA.WebForms.TagCodeBehindHandlers; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace CTA.WebForms.Services { /// /// Service class used to link code behind files to their corresponding /// view files. This makes simultaneous conversion of tags in the view /// layer and code behind references possible. /// public class CodeBehindReferenceLinkerService { private IDictionary _tagCodeBehindLinks; /// /// Initializes a new instance. /// public CodeBehindReferenceLinkerService() { _tagCodeBehindLinks = new Dictionary(); } /// /// Interacts with appropriate code behind converter to replace references /// to the given tag as specified by the provided code behind handler and /// retrieves a binding to the generated property if it exists. /// /// The path of the view file being modified. /// The id of the node being converted. /// The name of the attribute being converted as it will /// appear in the code behind. /// The converted source attribute value, should no /// code behind conversions be necessary. /// The attribute that the conversion result will be assigned to, /// if one exists, otherwise null. /// The code behind handler to be used on this node, if one exists. /// A cancellation token for stopping processes if stall occurs. /// Throws if view file with path /// has not been registered. /// Throws if is null. /// Property binding text if a bindable property was generated, null otherwise. public async Task HandleCodeBehindForAttributeAsync( string viewFilePath, string codeBehindName, string convertedSourceValue, string targetAttribute, TagCodeBehindHandler handler, CancellationToken token) { if (handler == null) { throw new ArgumentNullException($"{Rules.Config.Constants.WebFormsErrorTag}Failed to handle codebehind for attribute, " + $"argument {nameof(handler)} was null"); } if (!IsCodeBehindLinkCreated(viewFilePath)) { throw new InvalidOperationException($"{Rules.Config.Constants.WebFormsErrorTag}Failed to handle code behind conversions for " + $"attribute {codeBehindName} of element with ID {handler.IdValue}, missing view file registration for path {viewFilePath}"); } if (!IsCodeBehindLinkValid(viewFilePath)) { // If either the view file or code behind file doesn't // exist then do nothing, since no code behind conversions // can take place return null; } await WaitForClassDeclarationRegistered(viewFilePath, token); var semanticModel = _tagCodeBehindLinks[viewFilePath].SemanticModel; var classDeclaration = _tagCodeBehindLinks[viewFilePath].ClassDeclaration; handler.StageCodeBehindConversionsForAttribute( semanticModel, classDeclaration, codeBehindName, convertedSourceValue); return handler.GetBindingIfExists(codeBehindName, targetAttribute); } /// /// Stages operations to comment out any code behind references that are to be left /// unconverted by handlers pertaining to the given view file. Assumes that the /// view file at has already verified as been registered. /// /// The path of the view file whose code behind is to be cleaned up. private void StageCleanUpForUnconvertableReferences(string viewFilePath) { // Not checking contains key here because we assume it was checked beforehand, // failure to do this may result in an exception foreach (var handler in _tagCodeBehindLinks[viewFilePath].Handlers) { handler.StageCleanUpForUnconvertableReferences( _tagCodeBehindLinks[viewFilePath].SemanticModel, _tagCodeBehindLinks[viewFilePath].ClassDeclaration); } } /// /// Performs all staged operations in any handlers pertaining to the given view/code behind /// pairing. /// /// The path of the view file whose code behind is being converted. private void PerformAllStagedOperations(string viewFilePath) { // Not checking contains key here because we assume it was checked beforehand, // failure to do this may result in an exception var currentLink = _tagCodeBehindLinks[viewFilePath]; var allStagedConversions = currentLink.Handlers .SelectMany(handler => handler.StagedConversions) .DistinctBy(conversion => conversion.input) .ToDictionary(conversion => conversion.input, conversion => conversion.replacement); var alteredClass = currentLink.ClassDeclaration .ReplaceNodes(allStagedConversions.Keys, (original, _) => allStagedConversions[original]); foreach (var handler in currentLink.Handlers) { alteredClass = handler.PerformMemberAdditions(alteredClass); } _tagCodeBehindLinks[viewFilePath].ClassDeclaration = alteredClass; } /// /// Checks if a code behind link for files at the specified path has been /// created in the service. /// /// The path of the corresponding view file. /// true if the code behind link exists, false otherwise. private bool IsCodeBehindLinkCreated(string viewFilePath) { return _tagCodeBehindLinks.ContainsKey(viewFilePath); } /// /// Checks if a code behind link for files at the specified path validly /// links a code behind file and a view file. /// /// The path of the corresponding view file. /// true if the code behind link exists, and both files have been registered false otherwise. private bool IsCodeBehindLinkValid(string viewFilePath) { var isLinkCreated = IsCodeBehindLinkCreated(viewFilePath); var isViewFileExists = _tagCodeBehindLinks[viewFilePath].ViewFileExists; var isCodeBehindFIleExists = _tagCodeBehindLinks[viewFilePath].CodeBehindFileExists; var isValid = isLinkCreated && isViewFileExists && isCodeBehindFIleExists; if (!isValid) { LogHelper.LogWarning($"Invalid codebehind link found. Porting actions will not be applied to for {viewFilePath} or its codebehind. " + $"{nameof(isLinkCreated)}: {isLinkCreated}, " + $"{nameof(isViewFileExists)}: {isViewFileExists}, " + $"{nameof(isCodeBehindFIleExists)}: {isCodeBehindFIleExists}"); } return isValid; } /// /// Registers a view file for use by the service. /// /// The full path of the view file. public void RegisterViewFile(string viewFilePath) { if (!IsCodeBehindLinkCreated(viewFilePath)) { _tagCodeBehindLinks.Add(viewFilePath, new TagCodeBehindLink()); } _tagCodeBehindLinks[viewFilePath].ViewFileExists = true; } /// /// Registers a code behind file for use by the service. /// /// The full path of the view file. public void RegisterCodeBehindFile(string viewFilePath) { if (!IsCodeBehindLinkCreated(viewFilePath)) { _tagCodeBehindLinks.Add(viewFilePath, new TagCodeBehindLink()); } _tagCodeBehindLinks[viewFilePath].CodeBehindFileExists = true; } /// /// Registers a code behind handler that will be used in conversions for the given /// view file. /// /// The path of the view file that applies to. /// The code behind handler that is to be used for the file at . /// Throws if view file with path /// has not been registered. public void RegisterCodeBehindHandler(string viewFilePath, TagCodeBehindHandler handler) { if (!IsCodeBehindLinkCreated(viewFilePath)) { throw new InvalidOperationException($"{Rules.Config.Constants.WebFormsErrorTag}Failed to register code behind handler, " + $"missing view file registration for path {viewFilePath}"); } _tagCodeBehindLinks[viewFilePath].Handlers.Add(handler); } /// /// Registers a code behind class declaration that will be used by code behind handlers. /// /// The full path of the view file that this code behind is linked to. /// The semantic model that the belongs to. /// The code behind class declaration /// Throws if view file with path /// has not been registered. /// Throws if is null. public void RegisterClassDeclaration(string viewFilePath, SemanticModel semanticModel, ClassDeclarationSyntax classDeclaration) { if (semanticModel == null) { throw new ArgumentNullException($"{Rules.Config.Constants.WebFormsErrorTag}Failed to register code behind class declaration, " + $"argument {nameof(semanticModel)} was null"); } if (classDeclaration == null) { throw new ArgumentNullException($"{Rules.Config.Constants.WebFormsErrorTag}Failed to register code behind class declaration, " + $"argument {nameof(classDeclaration)} was null"); } if (!IsCodeBehindLinkCreated(viewFilePath)) { throw new InvalidOperationException($"{Rules.Config.Constants.WebFormsErrorTag}Failed to register type declaration, " + $"missing view file registration for path {viewFilePath}"); } _tagCodeBehindLinks[viewFilePath].SemanticModel = semanticModel; _tagCodeBehindLinks[viewFilePath].ClassDeclaration = classDeclaration; var classRegisteredSource = _tagCodeBehindLinks[viewFilePath].ClassDeclarationRegisteredTaskSource; if (!classRegisteredSource.Task.IsCompleted) { classRegisteredSource.SetResult(true); } } /// /// Notifies the service that the given view file has fully staged all of the code behind /// conversions available to its handlers, unblocking other processes that rely on this predicate. /// /// The full path of the view file that is being converted. /// Throws if view file with path /// has not been registered. public void NotifyAllHandlerConversionsStaged(string viewFilePath) { if (!IsCodeBehindLinkCreated(viewFilePath)) { throw new InvalidOperationException($"{Rules.Config.Constants.WebFormsErrorTag}Failed to raise handler conversions staged notification, " + $"missing view file registration for path {viewFilePath}"); } var handlerStagingSource = _tagCodeBehindLinks[viewFilePath].HandlersStagingTaskSource; if (!handlerStagingSource.Task.IsCompleted) { handlerStagingSource.SetResult(true); } } /// /// Converts tag code behind references in using code behind /// handlers necessary for the tags found in the view file at . /// /// The view file whose tag references are to be converted. /// The semantic model that the belongs to. /// The class declaration where the code behind references will be replaced. /// A cancellation token for stopping processes if stall occurs. /// The modified class declaration if modifications needed to be made, otherwise /// the original value of is returned. public async Task ExecuteTagCodeBehindHandlersAsync( string viewFilePath, SemanticModel semanticModel, ClassDeclarationSyntax classDeclaration, CancellationToken token) { if (IsCodeBehindLinkCreated(viewFilePath)) { RegisterClassDeclaration(viewFilePath, semanticModel, classDeclaration); await WaitForAllHandlerConversionsStaged(viewFilePath, token); StageCleanUpForUnconvertableReferences(viewFilePath); PerformAllStagedOperations(viewFilePath); return _tagCodeBehindLinks[viewFilePath].ClassDeclaration; } return classDeclaration; } /// /// A process used to block execution until all handlers for a given view file have staged their available /// conversions. Assumes that the view file at has already been verified as /// registered. /// /// The view file whose handlers are executing. /// A cancellation token for stopping processes if stall occurs. /// A task that completes once all handlers for a given view file have completed execution. private Task WaitForAllHandlerConversionsStaged(string viewFilePath, CancellationToken token) { // Not checking contains key here because we assume it was checked beforehand, // failure to do this may result in an exception var handlerStagingSource = _tagCodeBehindLinks[viewFilePath].HandlersStagingTaskSource; if (!handlerStagingSource.Task.IsCompleted) { token.Register(() => { if (!handlerStagingSource.Task.IsCompleted) { handlerStagingSource.SetCanceled(); } }); } return handlerStagingSource.Task; } /// /// A process used to block execution the code behind class declaration of a given view file /// has been registered. Assumes that the view file at has already /// been verified as registered. /// /// The view file whose code behind requires registration. /// A cancellation token for stopping processes if stall occurs. /// A task that completes once the code behind class declaration of a given view /// file has been registered. private Task WaitForClassDeclarationRegistered(string viewFilePath, CancellationToken token) { // Not checking contains key here because we assume it was checked beforehand, // failure to do this may result in an exception var classRegisteredSource = _tagCodeBehindLinks[viewFilePath].ClassDeclarationRegisteredTaskSource; if (!classRegisteredSource.Task.IsCompleted) { token.Register(() => { if (!classRegisteredSource.Task.IsCompleted) { classRegisteredSource.SetCanceled(); } }); } return classRegisteredSource.Task; } } }