Skip to content

Commit

Permalink
feat(fdc3):Initial AppDirectory implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
BalassaMarton committed Sep 18, 2023
1 parent 846f612 commit 8a6a291
Show file tree
Hide file tree
Showing 12 changed files with 617 additions and 2 deletions.
46 changes: 46 additions & 0 deletions src/fdc3/dotnet/AppDirectory/AppDirectory.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.7.34024.191
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppDirectory", "src\AppDirectory\AppDirectory.csproj", "{4B8CA047-2023-4E1A-BD73-85F7EB1D56B3}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9E52D00C-AABB-4579-8724-6CC7049F021E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppDirectory.Tests", "test\AppDirectory.Tests\AppDirectory.Tests.csproj", "{EAA7B5A8-FD08-4E2F-809B-6CBD489714C1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{62C29BA4-0D3B-4764-A898-38F29FF5C36C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MorganStanley.ComposeUI.Testing", "..\..\..\shared\dotnet\MorganStanley.ComposeUI.Testing\MorganStanley.ComposeUI.Testing.csproj", "{08AEA9CD-1DA8-4A73-BB10-90B0EF0EE8DC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4B8CA047-2023-4E1A-BD73-85F7EB1D56B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4B8CA047-2023-4E1A-BD73-85F7EB1D56B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4B8CA047-2023-4E1A-BD73-85F7EB1D56B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4B8CA047-2023-4E1A-BD73-85F7EB1D56B3}.Release|Any CPU.Build.0 = Release|Any CPU
{EAA7B5A8-FD08-4E2F-809B-6CBD489714C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EAA7B5A8-FD08-4E2F-809B-6CBD489714C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EAA7B5A8-FD08-4E2F-809B-6CBD489714C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EAA7B5A8-FD08-4E2F-809B-6CBD489714C1}.Release|Any CPU.Build.0 = Release|Any CPU
{08AEA9CD-1DA8-4A73-BB10-90B0EF0EE8DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{08AEA9CD-1DA8-4A73-BB10-90B0EF0EE8DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{08AEA9CD-1DA8-4A73-BB10-90B0EF0EE8DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{08AEA9CD-1DA8-4A73-BB10-90B0EF0EE8DC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{4B8CA047-2023-4E1A-BD73-85F7EB1D56B3} = {9E52D00C-AABB-4579-8724-6CC7049F021E}
{EAA7B5A8-FD08-4E2F-809B-6CBD489714C1} = {9E52D00C-AABB-4579-8724-6CC7049F021E}
{08AEA9CD-1DA8-4A73-BB10-90B0EF0EE8DC} = {62C29BA4-0D3B-4764-A898-38F29FF5C36C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D2C43446-C01E-438A-8A88-554F1351D134}
EndGlobalSection
EndGlobal
174 changes: 174 additions & 0 deletions src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Morgan Stanley makes this available to you under the Apache License,
// Version 2.0 (the "License"). You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0.
//
// See the NOTICE file distributed with this work for additional information
// regarding copyright ownership. Unless required by applicable law or agreed
// to in writing, software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions
// and limitations under the License.

using System.Collections.Concurrent;
using System.IO.Abstractions;
using System.Reactive.Disposables;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using MorganStanley.Fdc3.AppDirectory;
using MorganStanley.Fdc3.NewtonsoftJson.Serialization;
using Newtonsoft.Json;

namespace MorganStanley.ComposeUI.Fdc3.AppDirectory;

public class AppDirectory : IAppDirectory
{
public AppDirectory(
IOptions<AppDirectoryOptions> options,
IMemoryCache? cache = null,
IFileSystem? fileSystem = null,
ILogger<AppDirectory>? logger = null)
{
_options = options.Value;
_cache = cache ?? new MemoryCache(new MemoryCacheOptions());
_fileSystem = fileSystem ?? new FileSystem();
_logger = logger ?? NullLogger<AppDirectory>.Instance;
}

public async Task<IEnumerable<Fdc3App>> GetApps()
{
return (await GetAppsCore()).Values;
}

public async Task<Fdc3App?> GetApp(string appId)
{
if (!(await GetAppsCore()).TryGetValue(appId, out var app))
throw new AppNotFoundException(appId);

return app;

}

private readonly IFileSystem _fileSystem;
private readonly ILogger<AppDirectory> _logger;
private readonly AppDirectoryOptions _options;
private readonly IMemoryCache _cache;

private static readonly string GetAppsCacheKey = typeof(AppDirectory).FullName + "." + nameof(GetApps);

private Task<Dictionary<string, Fdc3App>> GetAppsCore()
{
return _cache.GetOrCreateAsync(
GetAppsCacheKey,
async entry =>
{
var result = await LoadApps();
entry.ExpirationTokens.Add(result.ChangeToken);

// Assuming that appIds are case-insensitive (not specified by the standard)
return result.Apps.ToDictionary(
app => app.AppId,
StringComparer.OrdinalIgnoreCase);
});
}

private Task<(IEnumerable<Fdc3App> Apps, IChangeToken ChangeToken)> LoadApps()
{
if (_fileSystem.File.Exists(_options.Source?.LocalPath))
{
return LoadAppsFromFile(_options.Source.LocalPath);
}
// TODO: add provider for online static files and FDC3 AppD API

_logger.LogWarning("The configured source is empty or not supported");

return Task.FromResult<(IEnumerable<Fdc3App>, IChangeToken)>(
(
Enumerable.Empty<Fdc3App>(),
NullChangeToken.Singleton));
}

private Task<(IEnumerable<Fdc3App>, IChangeToken)> LoadAppsFromFile(string fileName)
{
using var stream = _fileSystem.File.OpenRead(fileName);

return Task.FromResult<(IEnumerable<Fdc3App>, IChangeToken)>(
(
LoadAppsFromStream(stream),
new FileSystemChangeToken(fileName, _fileSystem)));
}

private static IEnumerable<Fdc3App> LoadAppsFromStream(Stream stream)
{
var serializer = JsonSerializer.Create(new Fdc3JsonSerializerSettings());
using var textReader = new StreamReader(stream, leaveOpen: true);
using var jsonReader = new JsonTextReader(textReader);

return serializer.Deserialize<IEnumerable<Fdc3App>>(jsonReader) ?? Enumerable.Empty<Fdc3App>();
}

private sealed class FileSystemChangeToken : IChangeToken
{
public FileSystemChangeToken(string path, IFileSystem fileSystem)
{
var fileSystemWatcher = fileSystem.FileSystemWatcher.New(
fileSystem.Path.GetDirectoryName(path)!,
fileSystem.Path.GetFileName(path));

fileSystemWatcher.Changed += (sender, args) =>
{
if (!args.ChangeType.HasFlag(WatcherChangeTypes.Changed))
return;

fileSystemWatcher.Dispose();
HasChanged = true;

foreach (var item in _callbacks)
{
item.Value();
}

_callbacks.Clear(); // Change tokens are only fired once
};

fileSystemWatcher.EnableRaisingEvents = true;
}

public IDisposable RegisterChangeCallback(Action<object> callback, object state)
{
if (HasChanged)
{
callback(state);

return Disposable.Empty;
}

var action = () => callback(state);

_callbacks.TryAdd(action, action);

return Disposable.Create(() => _callbacks.Remove(action, out _));
}

public bool HasChanged { get; private set; }
public bool ActiveChangeCallbacks => true;
private readonly ConcurrentDictionary<object, Action> _callbacks = new();
}

private sealed class NullChangeToken : IChangeToken
{
public IDisposable RegisterChangeCallback(Action<object> callback, object state)
{
return Disposable.Empty;
}

public bool HasChanged => false;
public bool ActiveChangeCallbacks => true;

public static readonly NullChangeToken Singleton = new();
}
}
30 changes: 30 additions & 0 deletions src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>MorganStanley.ComposeUI.Fdc3.AppDirectory</AssemblyName>
<RootNamespace>MorganStanley.ComposeUI.Fdc3.AppDirectory</RootNamespace>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
<PackageReference Include="MorganStanley.Fdc3.AppDirectory" Version="2.0.0-alpha.6" />
<PackageReference Include="MorganStanley.Fdc3.NewtonsoftJson" Version="2.0.0-alpha.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Roslyn.System.IO.Abstractions.Analyzers" Version="12.2.19">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Reactive" Version="6.0.0" />
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="19.2.69" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.69" />
</ItemGroup>

<ItemGroup>
<Folder Include="DependencyInjection\" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Morgan Stanley makes this available to you under the Apache License,
// Version 2.0 (the "License"). You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0.
//
// See the NOTICE file distributed with this work for additional information
// regarding copyright ownership. Unless required by applicable law or agreed
// to in writing, software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions
// and limitations under the License.

using Microsoft.Extensions.Options;
using MorganStanley.Fdc3.AppDirectory;

namespace MorganStanley.ComposeUI.Fdc3.AppDirectory;

/// <summary>
/// Configuration options for <see cref="AppDirectory"/>
/// </summary>
public sealed class AppDirectoryOptions : IOptions<AppDirectoryOptions>
{
/// <summary>
/// Gets or sets the source URL of a static JSON file.
/// </summary>
/// <remarks>
/// Supported schemes are <c>file</c>, <c>http</c> and <c>https</c>.
/// The static file must contain a single array of <see cref="Fdc3App"/> objects,
/// using the schema defined by the FDC3 AppDirectory API specification (https://fdc3.finos.org/schemas/2.0/app-directory.html#tag/Application/paths/~1v2~1apps~1%7BappId%7D/get)
/// UTF8 encoding is assumed unless a byte order mark or encoding header is present.
/// </remarks>
public Uri? Source { get; set; }

// TODO: Implement fetching from AppD REST API
///// <summary>
///// Gets or sets the name of the <see cref="HttpClient"/> that is used to fetch
///// the application definitions from a REST API defined by the FDC3 AppDirectory API specification (https://fdc3.finos.org/schemas/2.0/app-directory.html)
///// </summary>
///// <remarks>
///// This property is only used when <see cref="Source"/> is set to <c>null</c>.
///// When this property is set, an <see cref="IHttpClientFactory"/> instance must be provided as a constructor parameter
///// for <see cref="AppDirectory"/>, with a matching HTTP client configured.
///// </remarks>
//public string? HttpClientName { get; set; }

/// <inheritdoc/>
public AppDirectoryOptions Value => this;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Morgan Stanley makes this available to you under the Apache License,
// Version 2.0 (the "License"). You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0.
//
// See the NOTICE file distributed with this work for additional information
// regarding copyright ownership. Unless required by applicable law or agreed
// to in writing, software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions
// and limitations under the License.

namespace MorganStanley.ComposeUI.Fdc3.AppDirectory;

public class AppNotFoundException : Exception
{
public AppNotFoundException(string appId): base($"Could not find app with id '{appId}'")
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Morgan Stanley makes this available to you under the Apache License,
// Version 2.0 (the "License"). You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0.
//
// See the NOTICE file distributed with this work for additional information
// regarding copyright ownership. Unless required by applicable law or agreed
// to in writing, software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions
// and limitations under the License.

// ReSharper disable CheckNamespace
// ReSharper disable UnusedMember.Global
using Microsoft.Extensions.DependencyInjection.Extensions;
using MorganStanley.ComposeUI.Fdc3.AppDirectory;
using MorganStanley.Fdc3.AppDirectory;

namespace Microsoft.Extensions.DependencyInjection;

public static class ServiceCollectionAppDirectoryExtensions
{
public static IServiceCollection AddFdc3AppDirectory(this IServiceCollection serviceCollection)
{
serviceCollection.TryAddSingleton<IAppDirectory, AppDirectory>();

return serviceCollection;
}

public static IServiceCollection AddFdc3AppDirectory(
this IServiceCollection serviceCollection,
Action<AppDirectoryOptions> configureOptions)
{
serviceCollection.AddFdc3AppDirectory();
serviceCollection.AddOptions().Configure(configureOptions);

return serviceCollection;
}
}
Loading

0 comments on commit 8a6a291

Please sign in to comment.