Skip to content

Commit 4cc4b0d

Browse files
committed
gh-434 New APIs to get a list of input/output data plug-ins
New PluginAttribute to provide names for the plug-ins Signed-off-by: Victor Chang <[email protected]>
1 parent d54788e commit 4cc4b0d

14 files changed

+452
-7
lines changed

src/Api/PluginNameAttribute.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2023 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System;
18+
using Ardalis.GuardClauses;
19+
20+
namespace Monai.Deploy.InformaticsGateway.Api
21+
{
22+
public class PluginNameAttribute : Attribute
23+
{
24+
public string Name { get; set; }
25+
26+
public PluginNameAttribute(string name)
27+
{
28+
Guard.Against.NullOrWhiteSpace(name, nameof(name));
29+
30+
Name = name;
31+
}
32+
}
33+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2023 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using Microsoft.Extensions.Logging;
18+
19+
namespace Monai.Deploy.InformaticsGateway.Logging
20+
{
21+
public static partial class Log
22+
{
23+
[LoggerMessage(EventId = 5000, Level = LogLevel.Information, Message = "Loading assembly from {filename}.")]
24+
public static partial void LoadingAssembly(this ILogger logger, string filename);
25+
26+
[LoggerMessage(EventId = 5001, Level = LogLevel.Information, Message = "{type} data plug-in found {name}: {plugin}.")]
27+
public static partial void DataPluginFound(this ILogger logger, string type, string name, string plugin);
28+
29+
}
30+
}

src/InformaticsGateway/Logging/Log.8000.HttpServices.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ public static partial class Log
4242
[LoggerMessage(EventId = 8005, Level = LogLevel.Information, Message = "MONAI SCP AE Title {name} updated AE Title={aeTitle}.")]
4343
public static partial void MonaiApplicationEntityUpdated(this ILogger logger, string name, string aeTitle);
4444

45+
[LoggerMessage(EventId = 8006, Level = LogLevel.Error, Message = "Error reading data input plug-ins.")]
46+
public static partial void ErrorReadingDataInputPlugins(this ILogger logger, Exception ex);
47+
4548

4649
// Destination AE Title Controller
4750
[LoggerMessage(EventId = 8010, Level = LogLevel.Information, Message = "DICOM destination added AE Title={aeTitle}, Host/IP={hostIp}.")]

src/InformaticsGateway/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ internal static IHostBuilder CreateHostBuilder(string[] args) =>
113113
services.AddScoped<IPayloadNotificationActionHandler, PayloadNotificationActionHandler>();
114114
services.AddScoped<IInputDataPluginEngine, InputDataPluginEngine>();
115115
services.AddScoped<IOutputDataPluginEngine, OutputDataPluginEngine>();
116+
services.AddScoped<IDataPluginEngineFactory<IInputDataPlugin>, InputDataPluginEngineFactory>();
117+
services.AddScoped<IDataPluginEngineFactory<IOutputDataPlugin>, OutputDataPluginEngineFactory>();
116118

117119
services.AddMonaiDeployStorageService(hostContext.Configuration.GetSection("InformaticsGateway:storage:serviceAssemblyName").Value, Monai.Deploy.Storage.HealthCheckOptions.ServiceHealthCheck);
118120

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright 2023 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System;
18+
using System.Collections.Generic;
19+
using System.IO.Abstractions;
20+
using System.Linq;
21+
using System.Reflection;
22+
using Ardalis.GuardClauses;
23+
using Microsoft.Extensions.Logging;
24+
using Monai.Deploy.InformaticsGateway.Api;
25+
using Monai.Deploy.InformaticsGateway.Common;
26+
using Monai.Deploy.InformaticsGateway.Logging;
27+
28+
namespace Monai.Deploy.InformaticsGateway.Services.Common
29+
{
30+
public interface IDataPluginEngineFactory<T>
31+
{
32+
IReadOnlyDictionary<string, string> RegisteredPlugins();
33+
}
34+
35+
public abstract class DataPluginEngineFactoryBase<T> : IDataPluginEngineFactory<T>
36+
{
37+
private readonly IFileSystem _fileSystem;
38+
private readonly ILogger<DataPluginEngineFactoryBase<T>> _logger;
39+
private readonly Type _type;
40+
41+
/// <summary>
42+
/// A dictionary mapping of input data plug-ins where:
43+
/// key: <see cref="PluginNameAttribute.Name"/> if available or name of the class.
44+
/// value: fully qualified assembly type
45+
/// </summary>
46+
private readonly Dictionary<string, string> _cachedTypeNames;
47+
48+
public DataPluginEngineFactoryBase(IFileSystem fileSystem, ILogger<DataPluginEngineFactoryBase<T>> logger)
49+
{
50+
_fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
51+
_logger = logger ?? throw new System.ArgumentNullException(nameof(logger));
52+
_type = typeof(T);
53+
_cachedTypeNames = new Dictionary<string, string>();
54+
}
55+
56+
public IReadOnlyDictionary<string, string> RegisteredPlugins()
57+
{
58+
LoadAssembliesFromPluginDirectory();
59+
60+
var types = AppDomain.CurrentDomain.GetAssemblies()
61+
.SelectMany(s => s.GetTypes())
62+
.Where(p => _type.IsAssignableFrom(p) && p != _type).ToList();
63+
64+
AddToCache(types);
65+
66+
return _cachedTypeNames;
67+
}
68+
69+
private void AddToCache(List<Type> types)
70+
{
71+
Guard.Against.Null(types, nameof(types));
72+
73+
if (types.Any())
74+
{
75+
types.ForEach(p =>
76+
{
77+
if (!_cachedTypeNames.ContainsValue(p.AssemblyQualifiedName))
78+
{
79+
var nameAttribute = p.GetCustomAttribute<PluginNameAttribute>();
80+
81+
var name = nameAttribute is null ? p.Name : nameAttribute.Name;
82+
_cachedTypeNames.Add(name, p.AssemblyQualifiedName);
83+
_logger.DataPluginFound(_type.Name, name, p.AssemblyQualifiedName);
84+
}
85+
});
86+
}
87+
}
88+
89+
private void LoadAssembliesFromPluginDirectory()
90+
{
91+
var files = _fileSystem.Directory.GetFiles(SR.PlugInDirectoryPath, "*.dll", System.IO.SearchOption.TopDirectoryOnly);
92+
93+
foreach (var file in files)
94+
{
95+
_logger.LoadingAssembly(file);
96+
var assembly = Assembly.LoadFile(file);
97+
var matchingTypes = assembly.GetTypes().Where(p => _type.IsAssignableFrom(p) && p != _type).ToList();
98+
AddToCache(matchingTypes);
99+
}
100+
}
101+
}
102+
103+
public class InputDataPluginEngineFactory : DataPluginEngineFactoryBase<IInputDataPlugin>
104+
{
105+
public InputDataPluginEngineFactory(IFileSystem fileSystem, ILogger<DataPluginEngineFactoryBase<IInputDataPlugin>> logger) : base(fileSystem, logger)
106+
{
107+
}
108+
}
109+
110+
public class OutputDataPluginEngineFactory : DataPluginEngineFactoryBase<IOutputDataPlugin>
111+
{
112+
public OutputDataPluginEngineFactory(IFileSystem fileSystem, ILogger<DataPluginEngineFactoryBase<IOutputDataPlugin>> logger) : base(fileSystem, logger)
113+
{
114+
}
115+
}
116+
}

src/InformaticsGateway/Services/Http/DestinationAeTitleController.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
using Monai.Deploy.InformaticsGateway.Configuration;
2828
using Monai.Deploy.InformaticsGateway.Database.Api.Repositories;
2929
using Monai.Deploy.InformaticsGateway.Logging;
30+
using Monai.Deploy.InformaticsGateway.Services.Common;
3031
using Monai.Deploy.InformaticsGateway.Services.Scu;
3132

3233
namespace Monai.Deploy.InformaticsGateway.Services.Http
@@ -37,16 +38,19 @@ public class DestinationAeTitleController : ControllerBase
3738
{
3839
private readonly ILogger<DestinationAeTitleController> _logger;
3940
private readonly IDestinationApplicationEntityRepository _repository;
41+
private readonly IDataPluginEngineFactory<IOutputDataPlugin> _outputDataPluginEngineFactory;
4042
private readonly IScuQueue _scuQueue;
4143

4244
public DestinationAeTitleController(
4345
ILogger<DestinationAeTitleController> logger,
4446
IDestinationApplicationEntityRepository repository,
45-
IScuQueue scuQueue)
47+
IScuQueue scuQueue,
48+
IDataPluginEngineFactory<IOutputDataPlugin> outputDataPluginEngineFactory)
4649
{
4750
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
4851
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
4952
_scuQueue = scuQueue ?? throw new ArgumentNullException(nameof(scuQueue));
53+
_outputDataPluginEngineFactory = outputDataPluginEngineFactory ?? throw new ArgumentNullException(nameof(outputDataPluginEngineFactory));
5054
}
5155

5256
[HttpGet]
@@ -253,6 +257,23 @@ public async Task<ActionResult<DestinationApplicationEntity>> Delete(string name
253257
}
254258
}
255259

260+
[HttpGet("plug-ins")]
261+
[Produces("application/json")]
262+
[ProducesResponseType(StatusCodes.Status200OK)]
263+
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
264+
public ActionResult<MonaiApplicationEntity> GetPlugins()
265+
{
266+
try
267+
{
268+
return Ok(_outputDataPluginEngineFactory.RegisteredPlugins());
269+
}
270+
catch (Exception ex)
271+
{
272+
_logger.ErrorReadingDataInputPlugins(ex);
273+
return Problem(title: "Error reading data input plug-ins.", statusCode: (int)System.Net.HttpStatusCode.InternalServerError, detail: ex.Message);
274+
}
275+
}
276+
256277
private async Task ValidateCreateAsync(DestinationApplicationEntity item)
257278
{
258279
if (await _repository.ContainsAsync(p => p.Name.Equals(item.Name), HttpContext.RequestAborted).ConfigureAwait(false))

src/InformaticsGateway/Services/Http/MonaiAeTitleController.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
using System.Linq;
2020
using System.Net.Mime;
2121
using System.Threading.Tasks;
22-
using Amazon.Runtime.Internal;
2322
using Ardalis.GuardClauses;
2423
using Microsoft.AspNetCore.Http;
2524
using Microsoft.AspNetCore.Mvc;
@@ -29,6 +28,7 @@
2928
using Monai.Deploy.InformaticsGateway.Configuration;
3029
using Monai.Deploy.InformaticsGateway.Database.Api.Repositories;
3130
using Monai.Deploy.InformaticsGateway.Logging;
31+
using Monai.Deploy.InformaticsGateway.Services.Common;
3232
using Monai.Deploy.InformaticsGateway.Services.Scp;
3333

3434
namespace Monai.Deploy.InformaticsGateway.Services.Http
@@ -39,15 +39,18 @@ public class MonaiAeTitleController : ControllerBase
3939
{
4040
private readonly ILogger<MonaiAeTitleController> _logger;
4141
private readonly IMonaiApplicationEntityRepository _repository;
42+
private readonly IDataPluginEngineFactory<IInputDataPlugin> _inputDataPluginEngineFactory;
4243
private readonly IMonaiAeChangedNotificationService _monaiAeChangedNotificationService;
4344

4445
public MonaiAeTitleController(
4546
ILogger<MonaiAeTitleController> logger,
4647
IMonaiAeChangedNotificationService monaiAeChangedNotificationService,
47-
IMonaiApplicationEntityRepository repository)
48+
IMonaiApplicationEntityRepository repository,
49+
IDataPluginEngineFactory<IInputDataPlugin> inputDataPluginEngineFactory)
4850
{
4951
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
5052
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
53+
_inputDataPluginEngineFactory = inputDataPluginEngineFactory ?? throw new ArgumentNullException(nameof(inputDataPluginEngineFactory));
5154
_monaiAeChangedNotificationService = monaiAeChangedNotificationService ?? throw new ArgumentNullException(nameof(monaiAeChangedNotificationService));
5255
}
5356

@@ -205,6 +208,23 @@ public async Task<ActionResult<MonaiApplicationEntity>> Delete(string name)
205208
}
206209
}
207210

211+
[HttpGet("plug-ins")]
212+
[Produces("application/json")]
213+
[ProducesResponseType(StatusCodes.Status200OK)]
214+
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
215+
public ActionResult<MonaiApplicationEntity> GetPlugins()
216+
{
217+
try
218+
{
219+
return Ok(_inputDataPluginEngineFactory.RegisteredPlugins());
220+
}
221+
catch (Exception ex)
222+
{
223+
_logger.ErrorReadingDataInputPlugins(ex);
224+
return Problem(title: "Error reading data input plug-ins.", statusCode: (int)System.Net.HttpStatusCode.InternalServerError, detail: ex.Message);
225+
}
226+
}
227+
208228
private async Task ValidateCreateAsync(MonaiApplicationEntity item)
209229
{
210230
Guard.Against.Null(item, nameof(item));

src/InformaticsGateway/Test/Plugins/TestInputDataPlugins.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
namespace Monai.Deploy.InformaticsGateway.Test.Plugins
2222
{
23+
[PluginName("TestInputDataPluginAddWorkflow")]
2324
public class TestInputDataPluginAddWorkflow : IInputDataPlugin
2425
{
2526
public static readonly string TestString = "TestInputDataPlugin executed!";
@@ -30,10 +31,13 @@ public class TestInputDataPluginAddWorkflow : IInputDataPlugin
3031
return Task.FromResult((dicomFile, fileMetadata));
3132
}
3233
}
34+
35+
[PluginName("TestInputDataPluginResumeWorkflow")]
3336
public class TestInputDataPluginResumeWorkflow : IInputDataPlugin
3437
{
3538
public static readonly string WorkflowInstanceId = "ee04a4ac-abb3-412b-b3a7-662c96380379";
3639
public static readonly string TaskId = "45b20f97-2b38-4b9a-baeb-d15f9d496851";
40+
3741
public Task<(DicomFile dicomFile, FileStorageMetadata fileMetadata)> Execute(DicomFile dicomFile, FileStorageMetadata fileMetadata)
3842
{
3943
fileMetadata.WorkflowInstanceId = WorkflowInstanceId;
@@ -42,6 +46,7 @@ public class TestInputDataPluginResumeWorkflow : IInputDataPlugin
4246
}
4347
}
4448

49+
[PluginName("TestInputDataPluginModifyDicomFile")]
4550
public class TestInputDataPluginModifyDicomFile : IInputDataPlugin
4651
{
4752
public static readonly DicomTag ExpectedTag = DicomTag.PatientAddress;

src/InformaticsGateway/Test/Plugins/TestOutputDataPlugins.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
namespace Monai.Deploy.InformaticsGateway.Test.Plugins
2121
{
2222

23+
[PluginName("TestOutputDataPluginAddMessage")]
2324
public class TestOutputDataPluginAddMessage : IOutputDataPlugin
2425
{
2526
public static readonly string ExpectedValue = "Hello from TestOutputDataPluginAddMessage";
@@ -31,6 +32,7 @@ public class TestOutputDataPluginAddMessage : IOutputDataPlugin
3132
}
3233

3334
}
35+
[PluginName("TestOutputDataPluginModifyDicomFile")]
3436
public class TestOutputDataPluginModifyDicomFile : IOutputDataPlugin
3537
{
3638
public static readonly DicomTag ExpectedTag = DicomTag.PatientAddress;

src/InformaticsGateway/Test/ProgramTest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using System;
1818
using System.IO;
1919
using System.Reflection;
20+
using xRetry;
2021
using Xunit;
2122

2223
namespace Monai.Deploy.InformaticsGateway.Test
@@ -25,7 +26,7 @@ public class ProgramTest
2526
{
2627
private const string PlugInDirectoryName = "plug-ins";
2728

28-
[Fact(DisplayName = "Program - runs properly")]
29+
[RetryFact(DisplayName = "Program - runs properly")]
2930
public void Startup_RunsProperly()
3031
{
3132
var workingDirectory = Environment.CurrentDirectory;

0 commit comments

Comments
 (0)