Skip to content

Commit d8b353a

Browse files
committed
HL7v2 MLLP Listener (#103)
* gh-8 initial implementation of HL7 MLLP Frame listener * gh-8 Update integration test Signed-off-by: Victor Chang <[email protected]>
1 parent 42e4b44 commit d8b353a

34 files changed

+1372
-29
lines changed

.github/containerscan/allowedlist.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@
1414

1515
general:
1616
vulnerabilities:
17-
- CVE-2018-8292
17+
- CVE-2018-8292

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
*.db
2121
*.db-*
2222
src/InformaticsGateway/payloads
23-
23+
.run/
2424
# docfx temp files
2525
_site/
2626

@@ -559,4 +559,4 @@ FodyWeavers.xsd
559559
### VisualStudio Patch ###
560560
# Additional files built by Visual Studio
561561

562-
# End of https://www.toptal.com/developers/gitignore/api/aspnetcore,dotnetcore,visualstudio,visualstudiocode
562+
# End of https://www.toptal.com/developers/gitignore/api/aspnetcore,dotnetcore,visualstudio,visualstudiocode

docker-compose/docker-compose.dev.yml

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Copyright 2022 MONAI Consortium
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
version: "3.7"
16+
services:
17+
rabbitmq:
18+
image: rabbitmq:3-management
19+
hostname: rabbitmq
20+
ports:
21+
- 5672:5672
22+
- 15672:15672
23+
environment:
24+
RABBITMQ_DEFAULT_USER: "rabbitmq"
25+
RABBITMQ_DEFAULT_PASS: "rabbitmq"
26+
RABBITMQ_DEFAULT_VHOST: "monaideploy"
27+
RABBITMQ_ERLANG_COOKIE: "SWQOKODSQALRPCLNMEQG"
28+
29+
healthcheck:
30+
test: rabbitmq-diagnostics -q ping
31+
interval: 15s
32+
timeout: 30s
33+
retries: 3
34+
35+
minio:
36+
image: "minio/minio:latest"
37+
command: server --console-address ":9001" /data
38+
hostname: minio
39+
volumes:
40+
- ./.run/minio/data:/data
41+
- ./.run/minio/config:/root/.minio
42+
ports:
43+
- "9000:9000"
44+
environment:
45+
MINIO_ROOT_USER: minioadmin
46+
MINIO_ROOT_PASSWORD: minioadmin
47+
healthcheck:
48+
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
49+
interval: 15s
50+
timeout: 30s
51+
retries: 3
52+
53+
createbuckets:
54+
image: minio/mc
55+
environment:
56+
MINIO_ROOT_USER: minioadmin
57+
MINIO_ROOT_PASSWORD: minioadmin
58+
BUCKET_NAME: monaideploy
59+
ENDPOINT: http://minio:9000
60+
depends_on:
61+
minio:
62+
condition: service_healthy
63+
entrypoint: >
64+
/bin/sh -c "
65+
until (/usr/bin/mc config host add myminio $$ENDPOINT $$MINIO_ROOT_USER $$MINIO_ROOT_PASSWORD) do echo '...waiting...' && sleep 1; done;
66+
/usr/bin/mc mb myminio/$$BUCKET_NAME;
67+
/usr/bin/mc policy set public myminio/$$BUCKET_NAME;
68+
/usr/bin/mc ls myminio;
69+
# exit 0
70+
"
71+

src/Api/Monai.Deploy.InformaticsGateway.Api.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
<PackageReference Include="Macross.Json.Extensions" Version="3.0.0" />
3232
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="6.0.7" />
3333
<PackageReference Include="Monai.Deploy.Messaging" Version="0.1.3-rc0010" />
34-
<PackageReference Include="Monai.Deploy.Storage" Version="0.2.0-rc0012" />
34+
<PackageReference Include="Monai.Deploy.Storage" Version="0.2.0-rc0014" />
3535
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
3636
</ItemGroup>
3737

src/Configuration/ConfigurationValidator.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public ValidateOptionsResult Validate(string name, InformaticsGatewayConfigurati
5454
valid &= IsDicomWebValid(options.DicomWeb);
5555
valid &= IsFhirValid(options.Fhir);
5656
valid &= IsStorageValid(options.Storage);
57+
valid &= IsHl7Valid(options.Hl7);
5758

5859
#pragma warning disable CA2254 // Template should be a static expression
5960
_validationErrors.ForEach(p => _logger.Log(LogLevel.Error, p));
@@ -62,6 +63,14 @@ public ValidateOptionsResult Validate(string name, InformaticsGatewayConfigurati
6263
return valid ? ValidateOptionsResult.Success : ValidateOptionsResult.Fail(string.Join(Environment.NewLine, _validationErrors));
6364
}
6465

66+
private bool IsHl7Valid(Hl7Configuration hl7)
67+
{
68+
var valid = true;
69+
70+
valid &= ValidationExtensions.IsPortValid("InformaticsGateway>hl7>port", hl7.Port, _validationErrors);
71+
return valid;
72+
}
73+
6574
private bool IsStorageValid(StorageConfiguration storage)
6675
{
6776
var valid = true;

src/Configuration/Hl7Configuration.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// SPDX-FileCopyrightText: © 2022 MONAI Consortium
2+
// SPDX-License-Identifier: Apache License 2.0
3+
4+
using Microsoft.Extensions.Configuration;
5+
6+
namespace Monai.Deploy.InformaticsGateway.Configuration
7+
{
8+
public class Hl7Configuration
9+
{
10+
public static readonly int DefaultClientTimeout = 300000;
11+
public const int DefaultMaximumNumberOfConnections = 10;
12+
13+
/// <summary>
14+
/// Gets or sets the client connection timeout in milliseconds.
15+
/// </summary>
16+
[ConfigurationKeyName("clientTimeout")]
17+
public int ClientTimeoutMilliseconds { get; set; } = DefaultClientTimeout;
18+
19+
/// <summary>
20+
/// Gets or sets maximum number of concurrent connections for the HL7 service.
21+
/// Defaults to 10.
22+
/// </summary>
23+
[ConfigurationKeyName("maximumNumberOfConnections")]
24+
public int MaximumNumberOfConnections { get; set; } = DefaultMaximumNumberOfConnections;
25+
26+
/// <summary>
27+
/// Gets or sets the MLLP listening port.
28+
/// Defaults to 2575.
29+
/// </summary>
30+
[ConfigurationKeyName("clientTimeout")]
31+
public int Port { get; set; } = 2575;
32+
33+
/// <summary>
34+
/// Gets or sets wether to respond with an ack/nack message.
35+
/// Defaults to true.
36+
/// </summary>
37+
[ConfigurationKeyName("sendAck")]
38+
public bool SendAcknowledgment { get; set; } = true;
39+
40+
public uint BufferSize { get; set; } = 10240;
41+
42+
public Hl7Configuration()
43+
{
44+
}
45+
}
46+
}

src/Configuration/InformaticsGatewayConfiguration.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ public class InformaticsGatewayConfiguration
5656
[ConfigurationKeyName("fhir")]
5757
public FhirConfiguration Fhir { get; set; }
5858

59+
/// <summary>
60+
/// Represents the <c>hl7</c> section of the configuration file.
61+
/// </summary>
62+
/// <value></value>
63+
[ConfigurationKeyName("hl7")]
64+
public Hl7Configuration Hl7 { get; set; }
65+
5966
/// <summary>
6067
/// Represents the <c>export</c> section of the configuration file.
6168
/// </summary>
@@ -83,6 +90,7 @@ public InformaticsGatewayConfiguration()
8390
Export = new DataExportConfiguration();
8491
Messaging = new MessageBrokerConfiguration();
8592
Database = new DatabaseConfiguration();
93+
Hl7 = new Hl7Configuration();
8694
}
8795
}
8896
}

src/Configuration/Monai.Deploy.InformaticsGateway.Configuration.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
3232
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
3333
<PackageReference Include="Monai.Deploy.Messaging" Version="0.1.3-rc0010" />
34-
<PackageReference Include="Monai.Deploy.Storage" Version="0.2.0-rc0012" />
34+
<PackageReference Include="Monai.Deploy.Storage" Version="0.2.0-rc0014" />
3535
<PackageReference Include="System.IO.Abstractions" Version="17.0.24" />
3636
</ItemGroup>
3737

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// SPDX-FileCopyrightText: © 2022 MONAI Consortium
2+
// SPDX-License-Identifier: Apache License 2.0
3+
4+
using System;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace Monai.Deploy.InformaticsGateway.Logging
8+
{
9+
public static partial class Log
10+
{
11+
[LoggerMessage(EventId = 800, Level = LogLevel.Information, Message = "New HL7 client connected.")]
12+
public static partial void ClientConnected(this ILogger logger);
13+
14+
[LoggerMessage(EventId = 801, Level = LogLevel.Error, Message = "Error reading data, connection may be dropped.")]
15+
public static partial void ExceptionReadingClientStream(this ILogger logger, Exception ex);
16+
17+
[LoggerMessage(EventId = 802, Level = LogLevel.Error, Message = "Error parsing HL7 message.")]
18+
public static partial void ErrorParsingHl7Message(this ILogger logger, Exception ex);
19+
20+
[LoggerMessage(EventId = 803, Level = LogLevel.Warning, Message = "Unable to locate {segment} field {field} in the HL7 message.")]
21+
public static partial void MissingFieldInHL7Message(this ILogger logger, string segment, int field, Exception ex);
22+
23+
[LoggerMessage(EventId = 804, Level = LogLevel.Error, Message = "Error sending HL7 acknowledgment.")]
24+
public static partial void ErrorSendingHl7Acknowledgment(this ILogger logger, Exception ex);
25+
26+
[LoggerMessage(EventId = 805, Level = LogLevel.Information, Message = "Maximum number {maximumAllowedConcurrentConnections} of clients reached.")]
27+
public static partial void MaxedOutHl7Connections(this ILogger logger, int maximumAllowedConcurrentConnections);
28+
29+
[LoggerMessage(EventId = 806, Level = LogLevel.Information, Message = "HL7 listening on port: {port}.")]
30+
public static partial void Hl7ListeningOnPort(this ILogger logger, int port);
31+
32+
[LoggerMessage(EventId = 807, Level = LogLevel.Critical, Message = "Socket error: {error}")]
33+
public static partial void Hl7SocketException(this ILogger logger, string error);
34+
}
35+
}

src/InformaticsGateway/Monai.Deploy.InformaticsGateway.csproj

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!--
1+
<!--
22
~ Copyright 2022 MONAI Consortium
33
~
44
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -36,6 +36,7 @@
3636
<PackageReference Include="DotNext.Threading" Version="4.6.1" />
3737
<PackageReference Include="fo-dicom" Version="5.0.2" />
3838
<PackageReference Include="Karambolo.Extensions.Logging.File" Version="3.3.1" />
39+
<PackageReference Include="HL7-dotnetcore" Version="2.29.0" />
3940
<PackageReference Include="GitVersion.MsBuild" Version="5.10.3">
4041
<PrivateAssets>All</PrivateAssets>
4142
</PackageReference>
@@ -45,7 +46,9 @@
4546
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
4647
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
4748
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
48-
<PackageReference Include="Monai.Deploy.Storage" Version="0.2.0-rc0012" />
49+
<PackageReference Include="Monai.Deploy.Messaging.RabbitMQ" Version="0.1.3-rc0010" />
50+
<PackageReference Include="Monai.Deploy.Storage" Version="0.2.0-rc0014" />
51+
<PackageReference Include="Monai.Deploy.Storage.MinIO" Version="0.2.0-rc0014" />
4952
<PackageReference Include="Polly" Version="7.2.3" />
5053
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
5154
</ItemGroup>
@@ -78,4 +81,20 @@
7881
<None Include="./appsettings.json" CopyToOutputDirectory="Always" />
7982
<None Include="./appsettings.Development.json" CopyToOutputDirectory="Always" />
8083
</ItemGroup>
84+
85+
<Target Name="CopyPlugins" AfterTargets="AfterPublish">
86+
<ItemGroup>
87+
<PluginDlls Include="$(PublishDir)Monai.Deploy.Messaging.RabbitMQ.dll;$(PublishDir)Monai.Deploy.Storage.MinIO.dll;$(PublishDir)Minio.dll" />
88+
</ItemGroup>
89+
<Copy SourceFiles="@(PluginDlls)" DestinationFolder="$(PublishDir)\plug-ins\" SkipUnchangedFiles="true" />
90+
<Message Text="Files copied successfully to $(PublishDir)\plug-ins\." Importance="high" />
91+
</Target>
92+
93+
<Target Name="CopyPluginsBuild" AfterTargets="Build">
94+
<ItemGroup>
95+
<PluginDlls Include="$(OutDir)Monai.Deploy.Messaging.RabbitMQ.dll;$(OutDir)Monai.Deploy.Storage.MinIO.dll;$(OutDir)Minio.dll" />
96+
</ItemGroup>
97+
<Copy SourceFiles="@(PluginDlls)" DestinationFolder="$(OutDir)\plug-ins\" SkipUnchangedFiles="true" />
98+
<Message Text="Files copied successfully to $(OutDir)\plug-ins\." Importance="high" />
99+
</Target>
81100
</Project>

src/InformaticsGateway/Program.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@
3030
using Monai.Deploy.InformaticsGateway.Configuration;
3131
using Monai.Deploy.InformaticsGateway.Database;
3232
using Monai.Deploy.InformaticsGateway.Repositories;
33+
using Monai.Deploy.InformaticsGateway.Services.Common;
3334
using Monai.Deploy.InformaticsGateway.Services.Connectors;
3435
using Monai.Deploy.InformaticsGateway.Services.DicomWeb;
3536
using Monai.Deploy.InformaticsGateway.Services.Export;
37+
using Monai.Deploy.InformaticsGateway.Services.HealthLevel7;
3638
using Monai.Deploy.InformaticsGateway.Services.Http;
3739
using Monai.Deploy.InformaticsGateway.Services.Scp;
3840
using Monai.Deploy.InformaticsGateway.Services.Storage;
@@ -120,13 +122,16 @@ internal static IHostBuilder CreateHostBuilder(string[] args) =>
120122
services.AddSingleton<IMonaiServiceLocator, MonaiServiceLocator>();
121123
services.AddSingleton<IStorageInfoProvider, StorageInfoProvider>();
122124
services.AddSingleton<IMonaiAeChangedNotificationService, MonaiAeChangedNotificationService>();
125+
services.AddSingleton<ITcpListenerFactory, TcpListenerFactory>();
126+
services.AddSingleton<IMllpClientFactory, MllpClientFactory>();
123127
services.AddSingleton<IApplicationEntityManager, ApplicationEntityManager>();
124128
services.AddSingleton<SpaceReclaimerService>();
125129
services.AddSingleton<ScpService>();
126130
services.AddSingleton<ScuExportService>();
127131
services.AddSingleton<DicomWebExportService>();
128132
services.AddSingleton<DataRetrievalService>();
129133
services.AddSingleton<PayloadNotificationService>();
134+
services.AddSingleton<MllpService>();
130135

131136
var timeout = TimeSpan.FromSeconds(hostContext.Configuration.GetValue("InformaticsGateway:dicomWeb:clientTimeout", DicomWebConfiguration.DefaultClientTimeout));
132137
services
@@ -149,6 +154,7 @@ internal static IHostBuilder CreateHostBuilder(string[] args) =>
149154
services.AddHostedService<ScuExportService>(p => p.GetService<ScuExportService>());
150155
services.AddHostedService<DicomWebExportService>(p => p.GetService<DicomWebExportService>());
151156
services.AddHostedService<PayloadNotificationService>(p => p.GetService<PayloadNotificationService>());
157+
services.AddHostedService<MllpService>(p => p.GetService<MllpService>());
152158
})
153159
.ConfigureWebHostDefaults(webBuilder =>
154160
{
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// SPDX-FileCopyrightText: © 2022 MONAI Consortium
2+
// SPDX-License-Identifier: Apache License 2.0
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
8+
namespace Monai.Deploy.InformaticsGateway.Services.Common
9+
{
10+
internal interface INetworkStream : IDisposable
11+
{
12+
int ReadTimeout { get; set; }
13+
int WriteTimeout { get; set; }
14+
15+
ValueTask WriteAsync(ReadOnlyMemory<byte> ackData, CancellationToken cancellationToken = default);
16+
17+
Task FlushAsync(CancellationToken cancellationToken = default);
18+
19+
ValueTask<int> ReadAsync(Memory<byte> messageBuffer, CancellationToken cancellationToken = default);
20+
}
21+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// SPDX-FileCopyrightText: © 2022 MONAI Consortium
2+
// SPDX-License-Identifier: Apache License 2.0
3+
4+
using System;
5+
using System.Net;
6+
7+
namespace Monai.Deploy.InformaticsGateway.Services.Common
8+
{
9+
internal interface ITcpClientAdapter : IDisposable
10+
{
11+
EndPoint RemoteEndPoint { get; }
12+
13+
INetworkStream GetStream();
14+
}
15+
}

0 commit comments

Comments
 (0)