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;
}
}
}