Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FDC3 AppDirectory implementation #356

Merged
merged 3 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
173 changes: 173 additions & 0 deletions src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// 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 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");

Check warning on line 85 in src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs

View check run for this annotation

Codecov / codecov/patch

src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs#L85

Added line #L85 was not covered by tests

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

Check warning on line 90 in src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs

View check run for this annotation

Codecov / codecov/patch

src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs#L87-L90

Added lines #L87 - L90 were not covered by tests
}

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;

Check warning on line 123 in src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs

View check run for this annotation

Codecov / codecov/patch

src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs#L123

Added line #L123 was not covered by tests

fileSystemWatcher.Dispose();
HasChanged = true;

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

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

fileSystemWatcher.EnableRaisingEvents = true;
}

public bool HasChanged { get; private set; }
public bool ActiveChangeCallbacks => true;

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

Check warning on line 146 in src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs

View check run for this annotation

Codecov / codecov/patch

src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs#L146

Added line #L146 was not covered by tests

return Disposable.Empty;

Check warning on line 148 in src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs

View check run for this annotation

Codecov / codecov/patch

src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs#L148

Added line #L148 was not covered by tests
}

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

_callbacks.TryAdd(action, action);

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

private readonly ConcurrentDictionary<object, Action> _callbacks = new();
}

private sealed class NullChangeToken : IChangeToken
{
public bool HasChanged => false;
public bool ActiveChangeCallbacks => true;

Check warning on line 164 in src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs

View check run for this annotation

Codecov / codecov/patch

src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs#L163-L164

Added lines #L163 - L164 were not covered by tests

public IDisposable RegisterChangeCallback(Action<object> callback, object state)
{
return Disposable.Empty;

Check warning on line 168 in src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs

View check run for this annotation

Codecov / codecov/patch

src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs#L168

Added line #L168 was not covered by tests
}

public static readonly NullChangeToken Singleton = new();

Check warning on line 171 in src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs

View check run for this annotation

Codecov / codecov/patch

src/fdc3/dotnet/AppDirectory/src/AppDirectory/AppDirectory.cs#L171

Added line #L171 was not covered by tests
}
}
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>preview</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,49 @@
// 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,40 @@
// 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>();

Check warning on line 26 in src/fdc3/dotnet/AppDirectory/src/AppDirectory/DependencyInjection/ServiceCollectionAppDirectoryExtensions.cs

View check run for this annotation

Codecov / codecov/patch

src/fdc3/dotnet/AppDirectory/src/AppDirectory/DependencyInjection/ServiceCollectionAppDirectoryExtensions.cs#L26

Added line #L26 was not covered by tests

return serviceCollection;

Check warning on line 28 in src/fdc3/dotnet/AppDirectory/src/AppDirectory/DependencyInjection/ServiceCollectionAppDirectoryExtensions.cs

View check run for this annotation

Codecov / codecov/patch

src/fdc3/dotnet/AppDirectory/src/AppDirectory/DependencyInjection/ServiceCollectionAppDirectoryExtensions.cs#L28

Added line #L28 was not covered by tests
}

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

Check warning on line 36 in src/fdc3/dotnet/AppDirectory/src/AppDirectory/DependencyInjection/ServiceCollectionAppDirectoryExtensions.cs

View check run for this annotation

Codecov / codecov/patch

src/fdc3/dotnet/AppDirectory/src/AppDirectory/DependencyInjection/ServiceCollectionAppDirectoryExtensions.cs#L35-L36

Added lines #L35 - L36 were not covered by tests

return serviceCollection;

Check warning on line 38 in src/fdc3/dotnet/AppDirectory/src/AppDirectory/DependencyInjection/ServiceCollectionAppDirectoryExtensions.cs

View check run for this annotation

Codecov / codecov/patch

src/fdc3/dotnet/AppDirectory/src/AppDirectory/DependencyInjection/ServiceCollectionAppDirectoryExtensions.cs#L38

Added line #L38 was not covered by tests
}
}
Loading
Loading