diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml old mode 100644 new mode 100755 index ca6a5527c..51a009a7b --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -183,11 +183,11 @@ jobs: ports: - 27017:27017 steps: - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: zulu - java-version: '11' + java-version: '17' - uses: actions/setup-dotnet@v3 with: diff --git a/.gitignore b/.gitignore index 8c4eb593b..990b81827 100644 --- a/.gitignore +++ b/.gitignore @@ -560,3 +560,4 @@ FodyWeavers.xsd # Additional files built by Visual Studio # End of https://www.toptal.com/developers/gitignore/api/aspnetcore,dotnetcore,visualstudio,visualstudiocode +/src/InformaticsGateway/Properties/launchSettings.json diff --git a/doc/dependency_decisions.yml b/doc/dependency_decisions.yml index bee058ae7..dc7dcac0a 100755 --- a/doc/dependency_decisions.yml +++ b/doc/dependency_decisions.yml @@ -314,13 +314,15 @@ :why: MIT (https://raw.githubusercontent.com/dotnet/efcore/release/6.0/LICENSE.txt) :versions: - 6.0.22 - :when: 2022-08-16 23:05:49.698463427 Z + - 6.0.25 + - :when: 2022-08-16 23:05:49.698463427 Z - - :approve - Microsoft.EntityFrameworkCore - :who: mocsharp :why: MIT (https://raw.githubusercontent.com/dotnet/efcore/release/6.0/LICENSE.txt) :versions: - 6.0.22 + - 6.0.25 :when: 2022-08-16 23:05:50.137694970 Z - - :approve - Microsoft.EntityFrameworkCore.Abstractions @@ -328,41 +330,53 @@ :why: MIT (https://raw.githubusercontent.com/dotnet/efcore/release/6.0/LICENSE.txt) :versions: - 6.0.22 - :when: 2022-08-16 23:05:51.008105271 Z + - 6.0.25 + - :when: 2022-08-16 23:05:51.008105271 Z - - :approve - Microsoft.EntityFrameworkCore.Analyzers - :who: mocsharp :why: MIT (https://raw.githubusercontent.com/dotnet/efcore/release/6.0/LICENSE.txt) :versions: - 6.0.22 - :when: 2022-08-16 23:05:51.445711308 Z + - 6.0.25 + - :when: 2022-08-16 23:05:51.445711308 Z - - :approve - Microsoft.EntityFrameworkCore.Design - :who: mocsharp :why: MIT (https://raw.githubusercontent.com/dotnet/efcore/release/6.0/LICENSE.txt) :versions: - 6.0.22 - :when: 2022-08-16 23:05:51.922790944 Z + - 6.0.25 + - :when: 2022-08-16 23:05:51.922790944 Z - - :approve - Microsoft.EntityFrameworkCore.InMemory - :who: mocsharp :why: MIT (https://raw.githubusercontent.com/dotnet/efcore/release/6.0/LICENSE.txt) :versions: - 6.0.22 - :when: 2022-08-16 23:05:52.375150938 Z + - 6.0.25 + - :when: 2022-08-16 23:05:52.375150938 Z - - :approve - Microsoft.EntityFrameworkCore.Relational - :who: mocsharp :why: MIT (https://raw.githubusercontent.com/dotnet/efcore/release/6.0/LICENSE.txt) :versions: - 6.0.22 - :when: 2022-08-16 23:05:52.828879230 Z + - 6.0.25 + - :when: 2022-08-16 23:05:52.828879230 Z +- - :approve + - Microsoft.EntityFrameworkCore.Tools + - :who: ndsouth + :why: MIT (https://raw.githubusercontent.com/dotnet/efcore/release/6.0/LICENSE.txt) + :versions: + - 6.0.25 + - :when: 2022-08-16 23:05:52.828879230 Z - - :approve - Microsoft.EntityFrameworkCore.Sqlite - :who: mocsharp :why: MIT (https://raw.githubusercontent.com/dotnet/efcore/release/6.0/LICENSE.txt) :versions: - - 6.0.22 + - 6.0.25 :when: 2022-08-16 23:05:53.270526921 Z - - :approve - Microsoft.EntityFrameworkCore.Sqlite.Core @@ -370,6 +384,7 @@ :why: MIT (https://raw.githubusercontent.com/dotnet/efcore/release/6.0/LICENSE.txt) :versions: - 6.0.22 + - 6.0.25 :when: 2022-08-16 23:05:53.706997823 Z - - :approve - Microsoft.Extensions.ApiDescription.Server @@ -512,7 +527,8 @@ :versions: - 6.0.21 - 6.0.22 - :when: 2022-08-29 18:11:22.090772006 Z + - 6.0.25 + - :when: 2022-08-29 18:11:22.090772006 Z - - :approve - Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions - :who: mocsharp @@ -520,14 +536,16 @@ :versions: - 6.0.21 - 6.0.22 - :when: 2022-08-29 18:11:22.090772006 Z + - 6.0.25 + - :when: 2022-08-29 18:11:22.090772006 Z - - :approve - Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore - :who: mocsharp :why: MIT (https://github.com/dotnet/aspnetcore/raw/main/LICENSE.txt) :versions: - 6.0.22 - :when: 2022-08-29 18:11:22.090772006 Z + - 6.0.25 + - :when: 2022-08-29 18:11:22.090772006 Z - - :approve - Microsoft.Extensions.FileProviders.Abstractions - :who: mocsharp @@ -774,16 +792,16 @@ - :who: neilsouth :why: Apache-2.0 (https://github.com/Project-MONAI/monai-deploy-messaging/raw/main/LICENSE) :versions: - - 1.0.3 - - 1.0.4 + - 1.0.5-rc0006 + - 1.0.5 :when: 2023-10-13 18:06:21.511789690 Z - - :approve - Monai.Deploy.Messaging.RabbitMQ - :who: neilsouth :why: Apache-2.0 (https://github.com/Project-MONAI/monai-deploy-messaging/raw/main/LICENSE) :versions: - - 1.0.3 - - 1.0.4 + - 1.0.5-rc0006 + - 1.0.5 :when: 2023-10-13 18:06:21.511789690 Z - - :approve - Monai.Deploy.Storage diff --git a/src/Api/HL7DestinationEntity.cs b/src/Api/HL7DestinationEntity.cs new file mode 100644 index 000000000..33655bdde --- /dev/null +++ b/src/Api/HL7DestinationEntity.cs @@ -0,0 +1,40 @@ +/* + * Copyright 2021-2022 MONAI Consortium + * Copyright 2019-2020 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 Monai.Deploy.InformaticsGateway.Api.Models +{ + /// + /// HL7 Destination Entity + /// + /// + /// + /// { + /// "name": "MYPACS", + /// "hostIp": "10.20.100.200", + /// "aeTitle": "MONAIPACS", + /// "port": 1104 + /// } + /// + /// + public class HL7DestinationEntity : BaseApplicationEntity + { + /// + /// Gets or sets the port to connect to. + /// + public int Port { get; set; } + } +} diff --git a/src/Api/Hl7ApplicationConfigEntity.cs b/src/Api/Hl7ApplicationConfigEntity.cs new file mode 100755 index 000000000..3025cb967 --- /dev/null +++ b/src/Api/Hl7ApplicationConfigEntity.cs @@ -0,0 +1,170 @@ +/* + * Copyright 2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using FellowOakDicom; +using Monai.Deploy.InformaticsGateway.Api.Storage; +using Monai.Deploy.InformaticsGateway.Common; +using Newtonsoft.Json; + +namespace Monai.Deploy.InformaticsGateway.Api +{ + public class Hl7ApplicationConfigEntity : MongoDBEntityBase + { + /// + /// Gets or sets the name of a Hl7 application entity. + /// This value must be unique. + /// + [Key, Column(Order = 0)] + public string Name { get; set; } = default!; + + /// + /// Gets or sets the sending identifier. + /// + [JsonProperty("sending_identifier")] + public StringKeyValuePair SendingId { get; set; } = new(); + + /// + /// Gets or sets the data link. + /// Value is either PatientId or StudyInstanceUid + /// + [JsonProperty("data_link")] + public DataKeyValuePair DataLink { get; set; } = new(); + + /// + /// Gets or sets the data mapping. + /// Value is a DICOM Tag + /// + [JsonProperty("data_mapping")] + public List DataMapping { get; set; } = new(); + + /// + /// Optional list of data input plug-in type names to be executed by the . + /// + public List PlugInAssemblies { get; set; } = default!; + + public DateTime LastModified { get; set; } = DateTime.UtcNow; + + public IEnumerable Validate() + { + var errors = new List(); + if (string.IsNullOrWhiteSpace(SendingId.Key)) + errors.Add($"{nameof(SendingId.Key)} is missing."); + if (string.IsNullOrWhiteSpace(SendingId.Value)) + errors.Add($"{nameof(SendingId.Value)} is missing."); + + if (string.IsNullOrWhiteSpace(DataLink.Key)) + errors.Add($"{nameof(DataLink.Key)} is missing."); + + if (DataMapping.IsNullOrEmpty()) + errors.Add($"{nameof(DataMapping)} is missing values."); + + ValidateDataMapping(errors); + + return errors; + } + + private void ValidateDataMapping(List errors) + { + for (var idx = 0; idx < DataMapping.Count; idx++) + { + var dataMapKvp = DataMapping[idx]; + + if (string.IsNullOrWhiteSpace(dataMapKvp.Key) || dataMapKvp.Value.Length < 8) + { + if (string.IsNullOrWhiteSpace(dataMapKvp.Key)) + errors.Add($"{nameof(DataMapping)} is missing a name at index {idx}."); + + if (string.IsNullOrWhiteSpace(dataMapKvp.Value) || dataMapKvp.Value.Length < 8) + errors.Add($"{nameof(DataMapping)} ({dataMapKvp.Key}) @ index {idx} is not a valid DICOM Tag."); + + continue; + } + + try + { + DicomTag.Parse(dataMapKvp.Value); + } + catch (Exception e) + { + errors.Add($"DataMapping.Value is not a valid DICOM Tag. {e.Message}"); + } + } + } + + public override string ToString() + { + return JsonConvert.SerializeObject(this); + } + } + + //string key, string value + public class StringKeyValuePair : IKeyValuePair + { + [Key] + public string Key { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + + public static implicit operator StringKeyValuePair(KeyValuePair kvp) + { + return new StringKeyValuePair { Key = kvp.Key, Value = kvp.Value }; + } + + public static List FromDictionary(Dictionary dictionary) => + dictionary.Select(kvp => new StringKeyValuePair { Key = kvp.Key, Value = kvp.Value }).ToList(); + + public override bool Equals(object? obj) => Equals(obj as StringKeyValuePair); + + public bool Equals(StringKeyValuePair? other) => other != null && Key == other.Key && Value == other.Value; + + public override int GetHashCode() => HashCode.Combine(Key, Value); + + } + + public class DataKeyValuePair : IKeyValuePair + { + [Key] + public string Key { get; set; } = string.Empty; + public DataLinkType Value { get; set; } + + public static implicit operator DataKeyValuePair(KeyValuePair kvp) + { + return new DataKeyValuePair { Key = kvp.Key, Value = kvp.Value }; + } + + public override bool Equals(object? obj) => Equals(obj as DataKeyValuePair); + + public bool Equals(DataKeyValuePair? other) => other != null && Key == other.Key && Value == other.Value; + + public override int GetHashCode() => HashCode.Combine(Key, Value); + } + + public interface IKeyValuePair + { + public TKey Key { get; set; } + public TValue Value { get; set; } + } + + public enum DataLinkType + { + PatientId, + StudyInstanceUid + } +} diff --git a/src/InformaticsGateway/Services/HealthLevel7/IMllpClient.cs b/src/Api/Mllp/IMllpClient.cs old mode 100644 new mode 100755 similarity index 88% rename from src/InformaticsGateway/Services/HealthLevel7/IMllpClient.cs rename to src/Api/Mllp/IMllpClient.cs index 8e38ac846..e8c631c32 --- a/src/InformaticsGateway/Services/HealthLevel7/IMllpClient.cs +++ b/src/Api/Mllp/IMllpClient.cs @@ -18,9 +18,9 @@ using System.Threading; using System.Threading.Tasks; -namespace Monai.Deploy.InformaticsGateway.Services.HealthLevel7 +namespace Monai.Deploy.InformaticsGateway.Api.Mllp { - internal interface IMllpClient : IDisposable + public interface IMllpClient : IDisposable { Guid ClientId { get; } @@ -28,4 +28,4 @@ internal interface IMllpClient : IDisposable Task Start(Func onDisconnect, CancellationToken cancellationToken); } -} \ No newline at end of file +} diff --git a/src/Api/Mllp/IMllpExtract.cs b/src/Api/Mllp/IMllpExtract.cs new file mode 100755 index 000000000..2c82fee46 --- /dev/null +++ b/src/Api/Mllp/IMllpExtract.cs @@ -0,0 +1,30 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.Threading.Tasks; +using HL7.Dotnetcore; +using Monai.Deploy.InformaticsGateway.Api.Storage; + +namespace Monai.Deploy.InformaticsGateway.Api.Mllp +{ + public interface IMllpExtract + { + Task ExtractInfo(Hl7FileStorageMetadata meta, Message message, Hl7ApplicationConfigEntity configItem); + + Task GetConfigItem(Message message); + } +} diff --git a/src/Api/Mllp/IMllpService.cs b/src/Api/Mllp/IMllpService.cs new file mode 100755 index 000000000..d12e1fd28 --- /dev/null +++ b/src/Api/Mllp/IMllpService.cs @@ -0,0 +1,27 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace Monai.Deploy.InformaticsGateway.Api.Mllp +{ + public interface IMllpService + { + Task SendMllp(IPAddress address, int port, string hl7Message, CancellationToken cancellationToken); + } +} diff --git a/src/InformaticsGateway/Services/HealthLevel7/MllpClientResult.cs b/src/Api/Mllp/MllpClientResult.cs similarity index 91% rename from src/InformaticsGateway/Services/HealthLevel7/MllpClientResult.cs rename to src/Api/Mllp/MllpClientResult.cs index 401875804..36db3b65f 100755 --- a/src/InformaticsGateway/Services/HealthLevel7/MllpClientResult.cs +++ b/src/Api/Mllp/MllpClientResult.cs @@ -18,9 +18,9 @@ using System.Collections.Generic; using HL7.Dotnetcore; -namespace Monai.Deploy.InformaticsGateway.Services.HealthLevel7 +namespace Monai.Deploy.InformaticsGateway.Api.Mllp { - internal class MllpClientResult + public class MllpClientResult { public IList Messages { get; } public AggregateException? AggregateException { get; } diff --git a/src/Api/BaseApplicationEntity.cs b/src/Api/Models/BaseApplicationEntity.cs old mode 100644 new mode 100755 similarity index 96% rename from src/Api/BaseApplicationEntity.cs rename to src/Api/Models/BaseApplicationEntity.cs index ba0199ee6..0de7a3e39 --- a/src/Api/BaseApplicationEntity.cs +++ b/src/Api/Models/BaseApplicationEntity.cs @@ -17,8 +17,9 @@ using System; using System.Security.Claims; +using Monai.Deploy.InformaticsGateway.Api.Storage; -namespace Monai.Deploy.InformaticsGateway.Api +namespace Monai.Deploy.InformaticsGateway.Api.Models { /// /// DICOM Application Entity or AE. diff --git a/src/Api/DestinationApplicationEntity.cs b/src/Api/Models/DestinationApplicationEntity.cs old mode 100644 new mode 100755 similarity index 95% rename from src/Api/DestinationApplicationEntity.cs rename to src/Api/Models/DestinationApplicationEntity.cs index 6599591fa..4a7069edd --- a/src/Api/DestinationApplicationEntity.cs +++ b/src/Api/Models/DestinationApplicationEntity.cs @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Monai.Deploy.InformaticsGateway.Api +namespace Monai.Deploy.InformaticsGateway.Api.Models { /// /// Destination Application Entity diff --git a/src/Api/DicomAssociationInfo.cs b/src/Api/Models/DicomAssociationInfo.cs old mode 100644 new mode 100755 similarity index 94% rename from src/Api/DicomAssociationInfo.cs rename to src/Api/Models/DicomAssociationInfo.cs index d3a3eb775..e4f119ca6 --- a/src/Api/DicomAssociationInfo.cs +++ b/src/Api/Models/DicomAssociationInfo.cs @@ -16,8 +16,9 @@ using System; using System.Collections.Generic; +using Monai.Deploy.InformaticsGateway.Api.Storage; -namespace Monai.Deploy.InformaticsGateway.Api +namespace Monai.Deploy.InformaticsGateway.Api.Models { public class DicomAssociationInfo : MongoDBEntityBase { diff --git a/src/Api/ExportRequestDataMessage.cs b/src/Api/Models/ExportRequestDataMessage.cs similarity index 98% rename from src/Api/ExportRequestDataMessage.cs rename to src/Api/Models/ExportRequestDataMessage.cs index 891b1a8d8..9fd79d578 100755 --- a/src/Api/ExportRequestDataMessage.cs +++ b/src/Api/Models/ExportRequestDataMessage.cs @@ -20,7 +20,7 @@ using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.Messaging.Events; -namespace Monai.Deploy.InformaticsGateway.Api +namespace Monai.Deploy.InformaticsGateway.Api.Models { public class ExportRequestDataMessage { diff --git a/src/Api/Models/ExternalAppDetails.cs b/src/Api/Models/ExternalAppDetails.cs new file mode 100755 index 000000000..6353bc648 --- /dev/null +++ b/src/Api/Models/ExternalAppDetails.cs @@ -0,0 +1,39 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 Monai.Deploy.InformaticsGateway.Api.Storage; + +namespace Monai.Deploy.InformaticsGateway.Api.Models +{ + public class ExternalAppDetails : MongoDBEntityBase + { + public string StudyInstanceUid { get; set; } = string.Empty; + + public string StudyInstanceUidOutBound { get; set; } = string.Empty; + + public string WorkflowInstanceId { get; set; } = string.Empty; + + public string ExportTaskID { get; set; } = string.Empty; + + public string CorrelationId { get; set; } = string.Empty; + + public string? DestinationFolder { get; set; } + + public string PatientId { get; set; } = string.Empty; + + public string PatientIdOutBound { get; set; } = string.Empty; + } +} diff --git a/src/Api/MonaiApplicationEntity.cs b/src/Api/Models/MonaiApplicationEntity.cs similarity index 98% rename from src/Api/MonaiApplicationEntity.cs rename to src/Api/Models/MonaiApplicationEntity.cs index 9e2929147..8ec95e37d 100755 --- a/src/Api/MonaiApplicationEntity.cs +++ b/src/Api/Models/MonaiApplicationEntity.cs @@ -21,8 +21,9 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Security.Claims; using Monai.Deploy.InformaticsGateway.Api.PlugIns; +using Monai.Deploy.InformaticsGateway.Api.Storage; -namespace Monai.Deploy.InformaticsGateway.Api +namespace Monai.Deploy.InformaticsGateway.Api.Models { /// /// MONAI Application Entity diff --git a/src/Api/Monai.Deploy.InformaticsGateway.Api.csproj b/src/Api/Monai.Deploy.InformaticsGateway.Api.csproj index ed19368f5..181306f97 100755 --- a/src/Api/Monai.Deploy.InformaticsGateway.Api.csproj +++ b/src/Api/Monai.Deploy.InformaticsGateway.Api.csproj @@ -53,10 +53,11 @@ + - - - + + + diff --git a/src/Api/PlugIns/IInputHL7DataPlugIn.cs b/src/Api/PlugIns/IInputHL7DataPlugIn.cs new file mode 100755 index 000000000..b44d0f736 --- /dev/null +++ b/src/Api/PlugIns/IInputHL7DataPlugIn.cs @@ -0,0 +1,35 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.Threading.Tasks; +using HL7.Dotnetcore; +using Monai.Deploy.InformaticsGateway.Api.Storage; + +namespace Monai.Deploy.InformaticsGateway.Api.PlugIns +{ + /// + /// IInputDataPlugIn enables lightweight data processing over incoming data received from supported data ingestion + /// services. + /// Refer to for additional details. + /// + public interface IInputHL7DataPlugIn + { + string Name { get; } + + Task<(Message hl7Message, FileStorageMetadata fileMetadata)> ExecuteAsync(Message hl7File, FileStorageMetadata fileMetadata); + } + +} diff --git a/src/Api/PlugIns/IInputHL7DataPlugInEngine.cs b/src/Api/PlugIns/IInputHL7DataPlugInEngine.cs new file mode 100755 index 000000000..dc34b976d --- /dev/null +++ b/src/Api/PlugIns/IInputHL7DataPlugInEngine.cs @@ -0,0 +1,42 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.Collections.Generic; +using System.Threading.Tasks; +using HL7.Dotnetcore; +using Monai.Deploy.InformaticsGateway.Api.Storage; + +namespace Monai.Deploy.InformaticsGateway.Api.PlugIns +{ + /// + /// IInputDataPlugInEngine processes incoming data receivied from various supported services through + /// a list of plug-ins based on . + /// Rules: + /// + /// SCP: A list of plug-ins can be configured with each AET, and each plug-in is executed in the order stored, enabling piping of the incoming data before each file is uploaded to the storage service. + /// Incoming data is processed one file at a time and SHALL not wait for the entire study to arrive. + /// Plug-ins MUST be lightweight and not hinder the upload process. + /// Plug-ins SHALL not accumulate files in memory or storage for bulk processing. + /// + /// + public interface IInputHL7DataPlugInEngine + { + void Configure(IReadOnlyList pluginAssemblies); + + Task> ExecutePlugInsAsync(Message hl7File, FileStorageMetadata fileMetadata, Hl7ApplicationConfigEntity configItem); + } +} diff --git a/src/Api/PlugIns/IOutputDataPlugin.cs b/src/Api/PlugIns/IOutputDataPlugin.cs old mode 100644 new mode 100755 index 47d36da12..8bd695108 --- a/src/Api/PlugIns/IOutputDataPlugin.cs +++ b/src/Api/PlugIns/IOutputDataPlugin.cs @@ -16,6 +16,7 @@ using System.Threading.Tasks; using FellowOakDicom; +using Monai.Deploy.InformaticsGateway.Api.Models; namespace Monai.Deploy.InformaticsGateway.Api.PlugIns { diff --git a/src/Api/PlugIns/IOutputDataPluginEngine.cs b/src/Api/PlugIns/IOutputDataPluginEngine.cs old mode 100644 new mode 100755 index 07e62ccd0..080717a3d --- a/src/Api/PlugIns/IOutputDataPluginEngine.cs +++ b/src/Api/PlugIns/IOutputDataPluginEngine.cs @@ -16,6 +16,7 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Monai.Deploy.InformaticsGateway.Api.Models; namespace Monai.Deploy.InformaticsGateway.Api.PlugIns { diff --git a/src/Api/SourceApplicationEntity.cs b/src/Api/SourceApplicationEntity.cs index 604bf2eae..b46a61b8c 100755 --- a/src/Api/SourceApplicationEntity.cs +++ b/src/Api/SourceApplicationEntity.cs @@ -15,6 +15,8 @@ * limitations under the License. */ +using Monai.Deploy.InformaticsGateway.Api.Models; + namespace Monai.Deploy.InformaticsGateway.Api { /// diff --git a/src/Api/Storage/DicomFileStorageMetadata.cs b/src/Api/Storage/DicomFileStorageMetadata.cs old mode 100644 new mode 100755 index c6b0f4b39..9a01fda52 --- a/src/Api/Storage/DicomFileStorageMetadata.cs +++ b/src/Api/Storage/DicomFileStorageMetadata.cs @@ -36,7 +36,7 @@ public sealed record DicomFileStorageMetadata : FileStorageMetadata /// Gets or set the Study Instance UID of the DICOM instance. /// [JsonPropertyName("studyInstanceUid")] - public string StudyInstanceUid { get; init; } = default!; + public string StudyInstanceUid { get; set; } = default!; /// /// Gets or set the Series Instance UID of the DICOM instance. @@ -93,6 +93,15 @@ public DicomFileStorageMetadata(string associationId, string identifier, string StudyInstanceUid = studyInstanceUid; SeriesInstanceUid = seriesInstanceUid; SopInstanceUid = sopInstanceUid; + SetupFilePaths(associationId); + + DataOrigin.DataService = dataService; + DataOrigin.Source = callingAeTitle; + DataOrigin.Destination = calledAeTitle; + } + + private void SetupFilePaths(string associationId) + { File = new StorageObjectMetadata(FileExtension) { TemporaryPath = string.Join(PathSeparator, associationId, DataTypeDirectoryName, $"{Guid.NewGuid()}{FileExtension}"), @@ -106,16 +115,45 @@ public DicomFileStorageMetadata(string associationId, string identifier, string UploadPath = $"{File.UploadPath}{DicomJsonFileExtension}", ContentType = DicomJsonContentType, }; + } - DataOrigin.DataService = dataService; - DataOrigin.Source = callingAeTitle; - DataOrigin.Destination = calledAeTitle; + public void SetupGivenFilePaths(string? DestinationFolder) + { + if (DestinationFolder is null) + { + return; + } + + if (DestinationFolder.EndsWith('/')) + { + DestinationFolder = DestinationFolder.Remove(DestinationFolder.Length - 1); + } + + File = new StorageObjectMetadata(FileExtension) + { + TemporaryPath = string.Join(PathSeparator, DestinationFolder, $"Temp{PathSeparator}{Guid.NewGuid()}{FileExtension}"), + UploadPath = string.Join(PathSeparator, DestinationFolder, $"{SopInstanceUid}{FileExtension}"), + ContentType = DicomContentType, + DestinationFolderOverride = true, + }; + + JsonFile = new StorageObjectMetadata(DicomJsonFileExtension) + { + TemporaryPath = $"{File.TemporaryPath}{DicomJsonFileExtension}", + UploadPath = $"{File.UploadPath}{DicomJsonFileExtension}", + ContentType = DicomJsonContentType, + DestinationFolderOverride = true, + }; + + //DestinationFolderNeil = DestinationFolder; } + public void SetStudyInstanceUid(string newStudyInstanceUid) => StudyInstanceUid = newStudyInstanceUid; + public override void SetFailed() { base.SetFailed(); JsonFile.SetFailed(); } } -} \ No newline at end of file +} diff --git a/src/Api/Storage/FileStorageMetadata.cs b/src/Api/Storage/FileStorageMetadata.cs old mode 100644 new mode 100755 index 302612d07..62a530f1d --- a/src/Api/Storage/FileStorageMetadata.cs +++ b/src/Api/Storage/FileStorageMetadata.cs @@ -104,6 +104,9 @@ public abstract record FileStorageMetadata [JsonPropertyName("payloadId")] public string? PayloadId { get; set; } + // [JsonPropertyName("destinationFolder")] + //public string? DestinationFolderNeil { get; set; } + /// /// DO NOT USE /// This constructor is intended for JSON serializer. @@ -162,4 +165,4 @@ public static string IpAddress() return "127.0.0.1"; } } -} \ No newline at end of file +} diff --git a/src/Api/Storage/Hl7FileStorageMetadata.cs b/src/Api/Storage/Hl7FileStorageMetadata.cs old mode 100644 new mode 100755 index 2356f7738..576f88ae3 --- a/src/Api/Storage/Hl7FileStorageMetadata.cs +++ b/src/Api/Storage/Hl7FileStorageMetadata.cs @@ -54,6 +54,7 @@ public Hl7FileStorageMetadata(string connectionId, DataService dataType, string DataOrigin.DataService = dataType; DataOrigin.Source = dataOrigin; DataOrigin.Destination = IpAddress(); + DataOrigin.ArtifactType = Messaging.Common.ArtifactType.HL7; File = new StorageObjectMetadata(FileExtension) { @@ -63,4 +64,4 @@ public Hl7FileStorageMetadata(string connectionId, DataService dataType, string }; } } -} \ No newline at end of file +} diff --git a/src/Api/MongoDBEntityBase.cs b/src/Api/Storage/MongoDBEntityBase.cs old mode 100644 new mode 100755 similarity index 95% rename from src/Api/MongoDBEntityBase.cs rename to src/Api/Storage/MongoDBEntityBase.cs index 41b206a6e..1d2a38443 --- a/src/Api/MongoDBEntityBase.cs +++ b/src/Api/Storage/MongoDBEntityBase.cs @@ -16,7 +16,7 @@ using System; -namespace Monai.Deploy.InformaticsGateway.Api +namespace Monai.Deploy.InformaticsGateway.Api.Storage { public abstract class MongoDBEntityBase { diff --git a/src/Api/Storage/Payload.cs b/src/Api/Storage/Payload.cs index d88ffce7c..c390625a1 100755 --- a/src/Api/Storage/Payload.cs +++ b/src/Api/Storage/Payload.cs @@ -86,6 +86,8 @@ public TimeSpan Elapsed public int FilesFailedToUpload { get => Files.Count(p => p.IsUploadFailed); } + public string DestinationFolder { get; set; } = string.Empty; + public Payload(string key, string correlationId, string? workflowInstanceId, string? taskId, DataOrigin dataTrigger, uint timeout) { Guard.Against.NullOrWhiteSpace(key, nameof(key)); @@ -106,7 +108,7 @@ public Payload(string key, string correlationId, string? workflowInstanceId, str DataTrigger = dataTrigger; } - public Payload(string key, string correlationId, string? workflowInstanceId, string? taskId, DataOrigin dataTrigger, uint timeout, string? payloadId = null) : + public Payload(string key, string correlationId, string? workflowInstanceId, string? taskId, DataOrigin dataTrigger, uint timeout, string? payloadId = null, string? DestinationFolder = null) : this(key, correlationId, workflowInstanceId, taskId, dataTrigger, timeout) { Guard.Against.NullOrWhiteSpace(key, nameof(key)); @@ -119,6 +121,7 @@ public Payload(string key, string correlationId, string? workflowInstanceId, str { PayloadId = Guid.Parse(payloadId); } + DestinationFolder ??= string.Empty; } public void Add(FileStorageMetadata value) @@ -132,6 +135,11 @@ public void Add(FileStorageMetadata value) DataOrigins.Add(value.DataOrigin); } + //if (string.IsNullOrWhiteSpace(value.DestinationFolderNeil) is false) + //{ + // DestinationFolder = value.DestinationFolderNeil; + //} + _lastReceived.Reset(); _lastReceived.Start(); } diff --git a/src/Api/Storage/StorageObjectMetadata.cs b/src/Api/Storage/StorageObjectMetadata.cs index 93badfbf1..074463f2e 100755 --- a/src/Api/Storage/StorageObjectMetadata.cs +++ b/src/Api/Storage/StorageObjectMetadata.cs @@ -88,6 +88,9 @@ public class StorageObjectMetadata [JsonPropertyName("isMoveCompleted"), JsonInclude] public bool IsMoveCompleted { get; private set; } = default!; + [JsonPropertyName("destinationFolderOverride")] + public bool DestinationFolderOverride { get; set; } = false; + public StorageObjectMetadata(string fileExtension) { Guard.Against.NullOrWhiteSpace(fileExtension, nameof(fileExtension)); @@ -111,7 +114,11 @@ public string GetPayloadPath(Guid payloadId) { Guard.Against.Null(payloadId, nameof(payloadId)); - return $"{payloadId}{FileStorageMetadata.PathSeparator}{UploadPath}"; + if (DestinationFolderOverride is false) + { + return $"{payloadId}{FileStorageMetadata.PathSeparator}{UploadPath}"; + } + return $"{UploadPath}"; } public void SetUploaded(string bucketName) diff --git a/src/Api/Test/BaseApplicationEntityTest.cs b/src/Api/Test/BaseApplicationEntityTest.cs old mode 100644 new mode 100755 index ef9b14808..6fcfd032c --- a/src/Api/Test/BaseApplicationEntityTest.cs +++ b/src/Api/Test/BaseApplicationEntityTest.cs @@ -14,6 +14,7 @@ * limitations under the License. */ +using Monai.Deploy.InformaticsGateway.Api.Models; using Xunit; namespace Monai.Deploy.InformaticsGateway.Api.Test @@ -21,7 +22,7 @@ namespace Monai.Deploy.InformaticsGateway.Api.Test public class BaseApplicationEntityTest { [Fact] - public void GivenABaseApplicationEntity_WhenNameIsNotSet_ExepectSetDefaultValuesToSetName() + public void GivenABaseApplicationEntity_WhenNameIsNotSet_ExpectSetDefaultValuesToSetName() { var entity = new BaseApplicationEntity { @@ -35,7 +36,7 @@ public void GivenABaseApplicationEntity_WhenNameIsNotSet_ExepectSetDefaultValues } [Fact] - public void GivenABaseApplicationEntity_WhenNameIsSet_ExepectSetDefaultValuesToNotSetName() + public void GivenABaseApplicationEntity_WhenNameIsSet_ExpectSetDefaultValuesToNotSetName() { var entity = new BaseApplicationEntity { diff --git a/src/Api/Test/DestinationApplicationEntityTest.cs b/src/Api/Test/DestinationApplicationEntityTest.cs old mode 100644 new mode 100755 index 9e8287842..ec1cfe9be --- a/src/Api/Test/DestinationApplicationEntityTest.cs +++ b/src/Api/Test/DestinationApplicationEntityTest.cs @@ -14,6 +14,7 @@ * limitations under the License. */ +using Monai.Deploy.InformaticsGateway.Api.Models; using Xunit; namespace Monai.Deploy.InformaticsGateway.Api.Test @@ -21,7 +22,7 @@ namespace Monai.Deploy.InformaticsGateway.Api.Test public class MonaiApplicationEntityTest { [Fact] - public void GivenAMonaiApplicationEntity_WhenNameIsNotSet_ExepectSetDefaultValuesToBeUsed() + public void GivenAMonaiApplicationEntity_WhenNameIsNotSet_ExpectSetDefaultValuesToBeUsed() { var entity = new MonaiApplicationEntity { @@ -41,7 +42,7 @@ public void GivenAMonaiApplicationEntity_WhenNameIsNotSet_ExepectSetDefaultValue } [Fact] - public void GivenAMonaiApplicationEntity_WhenNameIsSet_ExepectSetDefaultValuesToNotOverwrite() + public void GivenAMonaiApplicationEntity_WhenNameIsSet_ExpectSetDefaultValuesToNotOverwrite() { var entity = new MonaiApplicationEntity { diff --git a/src/Api/Test/HL7DestinationEntityTest.cs b/src/Api/Test/HL7DestinationEntityTest.cs new file mode 100644 index 000000000..181f1fd9e --- /dev/null +++ b/src/Api/Test/HL7DestinationEntityTest.cs @@ -0,0 +1,54 @@ +/* + * Copyright 2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 Monai.Deploy.InformaticsGateway.Api.Models; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.Api.Test +{ + public class HL7DestinationEntityTest + { + [Fact] + public void GivenAMonaiApplicationEntity_WhenNameIsNotSet_ExepectSetDefaultValuesToBeUsed() + { + var entity = new HL7DestinationEntity + { + AeTitle = "AET", + }; + + entity.SetDefaultValues(); + + Assert.Equal(entity.AeTitle, entity.Name); + } + + [Fact] + public void GivenAMonaiApplicationEntity_WhenNameIsSet_ExepectSetDefaultValuesToNotOverwrite() + { + var entity = new HL7DestinationEntity + { + AeTitle = "AET", + HostIp = "IP", + Name = "Name" + }; + + entity.SetDefaultValues(); + + Assert.Equal("AET", entity.AeTitle); + Assert.Equal("IP", entity.HostIp); + Assert.Equal("Name", entity.Name); + } + } +} diff --git a/src/Api/Test/Hl7ApplicationConfigEntityTest.cs b/src/Api/Test/Hl7ApplicationConfigEntityTest.cs new file mode 100644 index 000000000..ae1a1bc2c --- /dev/null +++ b/src/Api/Test/Hl7ApplicationConfigEntityTest.cs @@ -0,0 +1,190 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * Copyright 2019-2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.Collections.Generic; +using Newtonsoft.Json; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.Api.Test +{ + public class Hl7ApplicationConfigEntityTest + { + [Fact] + public void GivenAHl7ApplicationConfigEntity_WhenSendingIdKeyIsNotSet_ExpectValidateToReturnError() + { + var entity = new Hl7ApplicationConfigEntity + { + SendingId = new KeyValuePair(string.Empty, "SendingIdValue"), + DataLink = new KeyValuePair("DataLinkKey", DataLinkType.PatientId), + DataMapping = StringKeyValuePair.FromDictionary(new Dictionary { { "DataMappingKey", "DataMappingValue" } }) + }; + + var errors = entity.Validate(); + + Assert.NotEmpty(errors); + Assert.Contains($"{nameof(entity.SendingId.Key)} is missing.", errors); + } + + [Fact] + public void GivenAHl7ApplicationConfigEntity_WhenSendingIdValueIsNotSet_ExpectValidateToReturnError() + { + var entity = new Hl7ApplicationConfigEntity + { + SendingId = new KeyValuePair("SendingIdKey", string.Empty), + DataLink = new KeyValuePair("DataLinkKey", DataLinkType.PatientId), + DataMapping = StringKeyValuePair.FromDictionary(new Dictionary { { "DataMappingKey", "DataMappingValue" } }) + }; + + var errors = entity.Validate(); + + Assert.NotEmpty(errors); + Assert.Contains($"{nameof(entity.SendingId.Value)} is missing.", errors); + } + + [Fact] + public void GivenAHl7ApplicationConfigEntity_WhenDataLinkKeyIsNotSet_ExpectValidateToReturnError() + { + var entity = new Hl7ApplicationConfigEntity + { + SendingId = new KeyValuePair("SendingIdKey", "SendingIdValue"), + DataLink = new KeyValuePair(string.Empty, DataLinkType.PatientId), + DataMapping = StringKeyValuePair.FromDictionary(new Dictionary { { "DataMappingKey", "DataMappingValue" } }) + }; + + var errors = entity.Validate(); + + Assert.NotEmpty(errors); + Assert.Contains($"{nameof(entity.DataLink.Key)} is missing.", errors); + } + + [Fact] + public void GivenAHl7ApplicationConfigEntity_WhenDataMappingIsNotSet_ExpectValidateToReturnError() + { + var entity = new Hl7ApplicationConfigEntity + { + SendingId = new KeyValuePair("SendingIdKey", "SendingIdValue"), + DataLink = new KeyValuePair("DataLinkKey", DataLinkType.PatientId), + DataMapping = StringKeyValuePair.FromDictionary(new Dictionary()) + }; + + var errors = entity.Validate(); + + Assert.NotEmpty(errors); + Assert.Contains($"{nameof(entity.DataMapping)} is missing values.", errors); + } + + [Fact] + public void GivenAHl7ApplicationConfigEntity_WhenDataMappingKeyIsNotSet_ExpectValidateToReturnError() + { + var entity = new Hl7ApplicationConfigEntity + { + SendingId = new KeyValuePair("SendingIdKey", "SendingIdValue"), + DataLink = new KeyValuePair("DataLinkKey", DataLinkType.PatientId), + DataMapping = StringKeyValuePair.FromDictionary(new Dictionary { { string.Empty, "DataMappingValue" } }) + }; + + var errors = entity.Validate(); + + Assert.NotEmpty(errors); + Assert.Contains($"{nameof(entity.DataMapping)} is missing a name at index 0.", errors); + } + + [Fact] + public void GivenAHl7ApplicationConfigEntity_WhenDataMappingValueIsNotSet_ExpectValidateToReturnError() + { + var entity = new Hl7ApplicationConfigEntity + { + SendingId = new KeyValuePair("SendingIdKey", "SendingIdValue"), + DataLink = new KeyValuePair("DataLinkKey", DataLinkType.PatientId), + DataMapping = StringKeyValuePair.FromDictionary(new Dictionary { { "DataMappingKey", string.Empty } }) + }; + + var errors = entity.Validate(); + + Assert.NotEmpty(errors); + Assert.Contains($"{nameof(entity.DataMapping)} (DataMappingKey) @ index 0 is not a valid DICOM Tag.", errors); + } + + [Fact] + public void GivenAHl7ApplicationConfigEntity_WhenDataMappingValueIsNotAValidDicomTag_ExpectValidateToReturnError() + { + var entity = new Hl7ApplicationConfigEntity + { + SendingId = new KeyValuePair("SendingIdKey", "SendingIdValue"), + DataLink = new KeyValuePair("DataLinkKey", DataLinkType.PatientId), + DataMapping = StringKeyValuePair.FromDictionary(new Dictionary { { "DataMappingKey", "DataMappingValue" } }) + }; + + var errors = entity.Validate(); + + Assert.NotEmpty(errors); + Assert.Contains("DataMapping.Value is not a valid DICOM Tag. Error parsing DICOM tag ['DataMappingValue']", errors); + } + + [Fact] + public void GivenAHl7ApplicationConfigEntity_WhenDataMappingValueIsAValidDicomTag_ExpectValidateToReturnNoErrors() + { + var entity = new Hl7ApplicationConfigEntity + { + SendingId = new KeyValuePair("SendingIdKey", "SendingIdValue"), + DataLink = new KeyValuePair("DataLinkKey", DataLinkType.PatientId), + DataMapping = StringKeyValuePair.FromDictionary(new Dictionary { { "DataMappingKey", "0020,000D" } }) + }; + + var errors = entity.Validate(); + + Assert.Empty(errors); + } + + [Fact] + public void GivenAHl7ApplicationConfigEntity_WhenDataMappingValueIsEmpty_ExpectValidateToReturnError() + { + var entity = new Hl7ApplicationConfigEntity + { + SendingId = new KeyValuePair("SendingIdKey", "SendingIdValue"), + DataLink = new KeyValuePair("DataLinkKey", DataLinkType.PatientId), + DataMapping = StringKeyValuePair.FromDictionary(new Dictionary { { "DataMappingKey", "" } }) + }; + + var errors = entity.Validate(); + + Assert.NotEmpty(errors); + Assert.Contains($"{nameof(entity.DataMapping)} (DataMappingKey) @ index 0 is not a valid DICOM Tag.", errors); + } + + [Fact] + public void GivenAHl7ApplicationConfigEntity_WhenToStringIsCalled_ExpectToStringToReturnExpectedValue() + { + var guid = Guid.NewGuid(); + var dt = DateTime.UtcNow; + var entity = new Hl7ApplicationConfigEntity + { + Id = guid, + DateTimeCreated = dt, + SendingId = new KeyValuePair("SendingIdKey", "SendingIdValue"), + DataLink = new KeyValuePair("DataLinkKey", DataLinkType.PatientId), + DataMapping = StringKeyValuePair.FromDictionary(new Dictionary { { "DataMappingKey", "0020,000D" } }) + }; + + var result = entity.ToString(); + + var expected = JsonConvert.SerializeObject(entity); + Assert.Equal(expected, result); + } + } +} diff --git a/src/Api/Test/MonaiApplicationEntityTest.cs b/src/Api/Test/MonaiApplicationEntityTest.cs old mode 100644 new mode 100755 index d91dd1166..1712d2690 --- a/src/Api/Test/MonaiApplicationEntityTest.cs +++ b/src/Api/Test/MonaiApplicationEntityTest.cs @@ -14,6 +14,7 @@ * limitations under the License. */ +using Monai.Deploy.InformaticsGateway.Api.Models; using Xunit; namespace Monai.Deploy.InformaticsGateway.Api.Test @@ -21,7 +22,7 @@ namespace Monai.Deploy.InformaticsGateway.Api.Test public class DestinationApplicationEntityTest { [Fact] - public void GivenADestinationApplicationEntity_WhenNameIsNotSet_ExepectSetDefaultValuesToSetName() + public void GivenADestinationApplicationEntity_WhenNameIsNotSet_ExpectSetDefaultValuesToSetName() { var entity = new DestinationApplicationEntity { @@ -36,7 +37,7 @@ public void GivenADestinationApplicationEntity_WhenNameIsNotSet_ExepectSetDefaul } [Fact] - public void GivenADestinationApplicationEntity_WhenNameIsSet_ExepectSetDefaultValuesToNotSetName() + public void GivenADestinationApplicationEntity_WhenNameIsSet_ExpectSetDefaultValuesToNotSetName() { var entity = new DestinationApplicationEntity { diff --git a/src/Api/Test/Storage/DicomFileStorageMetadataTest.cs b/src/Api/Test/Storage/DicomFileStorageMetadataTest.cs old mode 100644 new mode 100755 index 3753ddb59..836fd75c0 --- a/src/Api/Test/Storage/DicomFileStorageMetadataTest.cs +++ b/src/Api/Test/Storage/DicomFileStorageMetadataTest.cs @@ -89,5 +89,40 @@ public void GivenDicomFileStorageMetadata_WhenGetPayloadPathIsCalled_APayyloadPa Assert.Equal($"{payloadId}/{metadata.File.UploadPath}", metadata.File.GetPayloadPath(payloadId)); Assert.Equal($"{payloadId}/{metadata.JsonFile.UploadPath}", metadata.JsonFile.GetPayloadPath(payloadId)); } + + + [Fact] + public void StudyInstanceUid_Set_ValidValue() + { + // Arrange + var metadata = new DicomFileStorageMetadata(); + + // Act + metadata.StudyInstanceUid = "12345"; + + // Assert + Assert.Equal("12345", metadata.StudyInstanceUid); + } + + [Fact] + public void SeriesInstanceUid_Set_ValidValue() + { + // Arrange + var metadata = new DicomFileStorageMetadata { SeriesInstanceUid = "67890" }; + + // Assert + Assert.Equal("67890", metadata.SeriesInstanceUid); + } + + [Fact] + public void SopInstanceUid_Set_ValidValue() + { + // Arrange + var metadata = new DicomFileStorageMetadata { SopInstanceUid = "ABCDE" }; + + // Assert + Assert.Equal("ABCDE", metadata.SopInstanceUid); + } + } -} \ No newline at end of file +} diff --git a/src/Api/Test/packages.lock.json b/src/Api/Test/packages.lock.json index edc489b7e..903cc6b49 100755 --- a/src/Api/Test/packages.lock.json +++ b/src/Api/Test/packages.lock.json @@ -94,6 +94,11 @@ "System.Threading.Channels": "6.0.0" } }, + "HL7-dotnetcore": { + "type": "Transitive", + "resolved": "2.36.0", + "contentHash": "N1HLMeIqYuY+4O69ItgZJoDBnnpNkK5N2pClceTJ2nFJxsP48iCsA4iz3tm43Yszi4r/vaThoc3UoLBfGP3vKw==" + }, "Macross.Json.Extensions": { "type": "Transitive", "resolved": "3.0.0", @@ -116,8 +121,8 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", @@ -249,8 +254,8 @@ }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -260,10 +265,10 @@ }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } @@ -1277,11 +1282,12 @@ "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { + "HL7-dotnetcore": "[2.36.0, )", "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.22, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.25, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[1.0.4, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging": "[1.0.5, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Storage": "[0.2.18, )", "fo-dicom": "[5.1.1, )" } diff --git a/src/Api/VirtualApplicationEntity.cs b/src/Api/VirtualApplicationEntity.cs old mode 100644 new mode 100755 index 9a6545999..4fd33ffd1 --- a/src/Api/VirtualApplicationEntity.cs +++ b/src/Api/VirtualApplicationEntity.cs @@ -20,6 +20,7 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Security.Claims; using Monai.Deploy.InformaticsGateway.Api.PlugIns; +using Monai.Deploy.InformaticsGateway.Api.Storage; namespace Monai.Deploy.InformaticsGateway.Api { diff --git a/src/Api/packages.lock.json b/src/Api/packages.lock.json index 898a7a74c..1a4adf62a 100755 --- a/src/Api/packages.lock.json +++ b/src/Api/packages.lock.json @@ -21,6 +21,12 @@ "System.Threading.Channels": "6.0.0" } }, + "HL7-dotnetcore": { + "type": "Direct", + "requested": "[2.36.0, )", + "resolved": "2.36.0", + "contentHash": "N1HLMeIqYuY+4O69ItgZJoDBnnpNkK5N2pClceTJ2nFJxsP48iCsA4iz3tm43Yszi4r/vaThoc3UoLBfGP3vKw==" + }, "Macross.Json.Extensions": { "type": "Direct", "requested": "[3.0.0, )", @@ -29,15 +35,15 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Direct", - "requested": "[6.0.22, )", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "requested": "[6.0.25, )", + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Monai.Deploy.Messaging": { "type": "Direct", - "requested": "[1.0.4, )", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "requested": "[1.0.5, )", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -47,11 +53,11 @@ }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Direct", - "requested": "[1.0.4, )", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "requested": "[1.0.5, )", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } diff --git a/src/CLI/Commands/AetCommand.cs b/src/CLI/Commands/AetCommand.cs old mode 100644 new mode 100755 index 3c2f27c04..d47094b7c --- a/src/CLI/Commands/AetCommand.cs +++ b/src/CLI/Commands/AetCommand.cs @@ -26,7 +26,7 @@ using Ardalis.GuardClauses; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.CLI.Services; using Monai.Deploy.InformaticsGateway.Client; using Monai.Deploy.InformaticsGateway.Common; diff --git a/src/CLI/Commands/DestinationCommand.cs b/src/CLI/Commands/DestinationCommand.cs old mode 100644 new mode 100755 index 0205153f8..819cd18cf --- a/src/CLI/Commands/DestinationCommand.cs +++ b/src/CLI/Commands/DestinationCommand.cs @@ -26,7 +26,7 @@ using Ardalis.GuardClauses; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.CLI.Services; using Monai.Deploy.InformaticsGateway.Client; using Monai.Deploy.InformaticsGateway.Common; diff --git a/src/CLI/Services/ConfigurationOptionAccessor.cs b/src/CLI/Services/ConfigurationOptionAccessor.cs old mode 100644 new mode 100755 index c61ae761c..a086c6ece --- a/src/CLI/Services/ConfigurationOptionAccessor.cs +++ b/src/CLI/Services/ConfigurationOptionAccessor.cs @@ -30,6 +30,11 @@ public interface IConfigurationOptionAccessor /// int DicomListeningPort { get; set; } + /// + /// Gets or sets the ExternalApp DICOM SCP listening port from appsettings.json. + /// + int ExternalAppDicomListeningPort { get; set; } + /// /// Gets or sets the HL7 listening port from appsettings.json. /// @@ -112,6 +117,21 @@ public int DicomListeningPort } } + public int ExternalAppDicomListeningPort + { + get + { + return GetValueFromJsonPath("InformaticsGateway.dicom.scp.externalAppPort"); + } + set + { + Guard.Against.OutOfRangePort(value, nameof(ExternalAppDicomListeningPort)); + var jObject = ReadConfigurationFile(); + jObject["InformaticsGateway"]["dicom"]["scp"]["externalAppPort"] = value; + SaveConfigurationFile(jObject); + } + } + public int Hl7ListeningPort { get diff --git a/src/CLI/Test/AetCommandTest.cs b/src/CLI/Test/AetCommandTest.cs old mode 100644 new mode 100755 index 473f02426..9852ccf0a --- a/src/CLI/Test/AetCommandTest.cs +++ b/src/CLI/Test/AetCommandTest.cs @@ -28,7 +28,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.CLI.Services; using Monai.Deploy.InformaticsGateway.Client; using Monai.Deploy.InformaticsGateway.SharedTest; diff --git a/src/CLI/Test/DestinationCommandTest.cs b/src/CLI/Test/DestinationCommandTest.cs old mode 100644 new mode 100755 index 4212a7e0d..0d9dd4cd0 --- a/src/CLI/Test/DestinationCommandTest.cs +++ b/src/CLI/Test/DestinationCommandTest.cs @@ -29,7 +29,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.CLI.Services; using Monai.Deploy.InformaticsGateway.Client; using Monai.Deploy.InformaticsGateway.SharedTest; diff --git a/src/CLI/Test/packages.lock.json b/src/CLI/Test/packages.lock.json index 15a8c0324..dbdd6f20e 100755 --- a/src/CLI/Test/packages.lock.json +++ b/src/CLI/Test/packages.lock.json @@ -137,6 +137,11 @@ "System.Threading.Channels": "6.0.0" } }, + "HL7-dotnetcore": { + "type": "Transitive", + "resolved": "2.36.0", + "contentHash": "N1HLMeIqYuY+4O69ItgZJoDBnnpNkK5N2pClceTJ2nFJxsP48iCsA4iz3tm43Yszi4r/vaThoc3UoLBfGP3vKw==" + }, "Macross.Json.Extensions": { "type": "Transitive", "resolved": "3.0.0", @@ -164,8 +169,8 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Microsoft.Extensions.Configuration": { "type": "Transitive", @@ -497,8 +502,8 @@ }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -508,10 +513,10 @@ }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } @@ -1560,11 +1565,12 @@ "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { + "HL7-dotnetcore": "[2.36.0, )", "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.22, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.25, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[1.0.4, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging": "[1.0.5, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Storage": "[0.2.18, )", "fo-dicom": "[5.1.1, )" } diff --git a/src/CLI/packages.lock.json b/src/CLI/packages.lock.json index f45d6d7b6..22c1f3e80 100755 --- a/src/CLI/packages.lock.json +++ b/src/CLI/packages.lock.json @@ -93,6 +93,11 @@ "System.Threading.Channels": "6.0.0" } }, + "HL7-dotnetcore": { + "type": "Transitive", + "resolved": "2.36.0", + "contentHash": "N1HLMeIqYuY+4O69ItgZJoDBnnpNkK5N2pClceTJ2nFJxsP48iCsA4iz3tm43Yszi4r/vaThoc3UoLBfGP3vKw==" + }, "Macross.Json.Extensions": { "type": "Transitive", "resolved": "3.0.0", @@ -115,8 +120,8 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Microsoft.Extensions.Configuration": { "type": "Transitive", @@ -399,8 +404,8 @@ }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -410,10 +415,10 @@ }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } @@ -541,11 +546,12 @@ "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { + "HL7-dotnetcore": "[2.36.0, )", "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.22, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.25, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[1.0.4, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging": "[1.0.5, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Storage": "[0.2.18, )", "fo-dicom": "[5.1.1, )" } diff --git a/src/Client/IInformaticsGatewayClient.cs b/src/Client/IInformaticsGatewayClient.cs old mode 100644 new mode 100755 index cf9e8ef7d..3db89516e --- a/src/Client/IInformaticsGatewayClient.cs +++ b/src/Client/IInformaticsGatewayClient.cs @@ -17,6 +17,7 @@ using System; using System.Net.Http.Headers; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Client.Services; namespace Monai.Deploy.InformaticsGateway.Client @@ -53,6 +54,11 @@ public interface IInformaticsGatewayClient /// IAeTitleService VirtualAeTitle { get; } + /// + /// Provides APIs to list, create, delete Virtual AE Titles. + /// + IAeTitleService HL7Destinations { get; } + /// /// Configures the service URI of the DICOMweb service. /// diff --git a/src/Client/InformaticsGatewayClient.cs b/src/Client/InformaticsGatewayClient.cs old mode 100644 new mode 100755 index 9f48dac6c..fe9a81145 --- a/src/Client/InformaticsGatewayClient.cs +++ b/src/Client/InformaticsGatewayClient.cs @@ -20,6 +20,7 @@ using Ardalis.GuardClauses; using Microsoft.Extensions.Logging; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Client.Common; using Monai.Deploy.InformaticsGateway.Client.Services; @@ -48,6 +49,9 @@ public class InformaticsGatewayClient : IInformaticsGatewayClient /// public IAeTitleService VirtualAeTitle { get; } + /// + public IAeTitleService HL7Destinations { get; } + /// /// Initializes a new instance of the InformaticsGatewayClient class that connects to the specified URI using the credentials provided. /// @@ -66,6 +70,7 @@ public InformaticsGatewayClient(HttpClient httpClient, ILogger("config/source", _httpClient, _logger); DicomDestinations = new AeTitleService("config/destination", _httpClient, _logger); VirtualAeTitle = new AeTitleService("config/vae", _httpClient, _logger); + HL7Destinations = new AeTitleService("config/hl7-destination", _httpClient, _logger); } /// diff --git a/src/Client/Services/AeTitle{T}Service.cs b/src/Client/Services/AeTitle{T}Service.cs old mode 100644 new mode 100755 index 43a2e7f3d..e9df749ec --- a/src/Client/Services/AeTitle{T}Service.cs +++ b/src/Client/Services/AeTitle{T}Service.cs @@ -23,7 +23,7 @@ using System.Threading.Tasks; using Ardalis.GuardClauses; using Microsoft.Extensions.Logging; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; namespace Monai.Deploy.InformaticsGateway.Client.Services { diff --git a/src/Client/Test/AeTitleServiceTest.cs b/src/Client/Test/AeTitleServiceTest.cs old mode 100644 new mode 100755 index 646f03a44..df852a205 --- a/src/Client/Test/AeTitleServiceTest.cs +++ b/src/Client/Test/AeTitleServiceTest.cs @@ -24,6 +24,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Client.Common; using Monai.Deploy.InformaticsGateway.Client.Services; using Moq; diff --git a/src/Client/Test/packages.lock.json b/src/Client/Test/packages.lock.json index 36843f9e7..2bbd6ee7a 100755 --- a/src/Client/Test/packages.lock.json +++ b/src/Client/Test/packages.lock.json @@ -176,19 +176,19 @@ }, "Microsoft.Data.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "gtIGHbGnRq/h4mFSJYr9BdMObvJV/a67nBubs50VjPDusQARtWJzeVTirDWsbL1qTvGzbbZCD7VE7+s2ixZfow==", + "resolved": "6.0.25", + "contentHash": "rbXNoMg/ylGyJxLcyetojuXFzvDG85M31DfFbqL8veN4P8oG6wmnPwWNn3/bDIEDVvdw15R092dxpobQeQcjGg==", "dependencies": { "SQLitePCLRaw.core": "2.1.2" } }, "Microsoft.EntityFrameworkCore": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "vNe+y8ZsEf1CsfmfYttfKAz/IgCCtphgguvao0HWNJNdjZf9cabD288nZJ17b/WaQMWXhLwYAsofk8vNVkfTOA==", + "resolved": "6.0.25", + "contentHash": "txcqw2xrmvMoTIgzAdUk8JHLELofGgTK3i6glswVZs4SC8BOU1M/iSAtwMIVtAtfzxuBIUAbHPx+Ly6lfkYe7g==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "6.0.22", - "Microsoft.EntityFrameworkCore.Analyzers": "6.0.22", + "Microsoft.EntityFrameworkCore.Abstractions": "6.0.25", + "Microsoft.EntityFrameworkCore.Analyzers": "6.0.25", "Microsoft.Extensions.Caching.Memory": "6.0.1", "Microsoft.Extensions.DependencyInjection": "6.0.1", "Microsoft.Extensions.Logging": "6.0.0", @@ -198,39 +198,39 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "82SZfdrLe7bdDB8/3INV0UULvlUzsdHkrEYylDCrzFXRWHXG9eO5jJQjRHU8j9XkGIN+MSPgIlczBnqeDvB36A==" + "resolved": "6.0.25", + "contentHash": "i6UpdWqWxSBbIFOkaMoubM40yIjTZO+0rIUkY5JRltSeFI4PzncBBQcNVNXXjAmiLXF/xY0xTS+ykClbkV46Yg==" }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "W7yfdEbEuS1OPPxU0EOA6haqI4uvzs7OwHKh81DiJFn3NFNP2ztSovkOzBDhTwHX0j+OySsAj3BEJhuzTVYIVw==", + "resolved": "6.0.25", + "contentHash": "ci2lR++x7R7LR71+HoeRnB9Z5VeOQ1ILLbFRhsjjWZyLrAMkdq7TK9Ll47jo1TXDWF8Ddeap1JgcptgPKkWSRA==", "dependencies": { - "Microsoft.EntityFrameworkCore": "6.0.22", + "Microsoft.EntityFrameworkCore": "6.0.25", "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" } }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "EDKnYZtxq7P131xxLsEokda86WnFRiVAveLVAYR8kzyWl/UwTpf/RS2m2FrbH/U8vX3A+IQNpabtxcjtCUrY0g==", + "resolved": "6.0.25", + "contentHash": "vaQNuXgUN0nIzFXQiPSb9iAaJqLvZA164Sx9mjF5rFQS5cwQ/AiymF0e4J0QH3P07Mf3zEVZE5u2fTO0NacuMQ==", "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.22", + "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.25", "SQLitePCLRaw.bundle_e_sqlite3": "2.1.2" } }, "Microsoft.EntityFrameworkCore.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "xSU77ORQgwlD+s5Cmlk9DzoSCu5oxlHLuQl+v5zAZ0Uv5yH17hp02TBfz3x9nBA+CrIsqaLjGEuyZmLDf/5ATw==", + "resolved": "6.0.25", + "contentHash": "IU4E8I9FS2sUVxJJ0w/4jogLQ8C0zvu/SO6b1tRmiiCtTrHhjUB0tqhxjrFnDXZ/mpCJOElw50+qhbcElm0CYw==", "dependencies": { - "Microsoft.Data.Sqlite.Core": "6.0.22", - "Microsoft.EntityFrameworkCore.Relational": "6.0.22", + "Microsoft.Data.Sqlite.Core": "6.0.25", + "Microsoft.EntityFrameworkCore.Relational": "6.0.25", "Microsoft.Extensions.DependencyModel": "6.0.0" } }, @@ -336,10 +336,10 @@ }, "Microsoft.Extensions.Diagnostics.HealthChecks": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "HB1Zp1NY9m+HwYKLZBgUfNIt0xXzm4APARDuAIPODl8pT4g10oOiEDN8asOzx/sfL9xM+Sse5Zne9L+6qYi/iA==", + "resolved": "6.0.25", + "contentHash": "9vz47iGkzqhh0bGqomOTxaJNEEajeNcbSTSWwhh9Soo9lWm0UdPbw04CxXCQJPhc0aw9OaMnOxx7sCcde8/adA==", "dependencies": { - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.22", + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.25", "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", "Microsoft.Extensions.Logging.Abstractions": "6.0.4", "Microsoft.Extensions.Options": "6.0.0" @@ -347,17 +347,17 @@ }, "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "yvz+0r3qAt6gNEKlGSBO1BXMhtD3Tt8yzU59dHASolpwlSHvgqy0tEP6KXn3MPoKlPr0CiAHUdzOwYSoljzRSg==" + "resolved": "6.0.25", + "contentHash": "9sd1K/rp/vlxrBWNa0i8fgHCBPg94cocGMsJr7z9e2zQGQxMHNGpspdcy/FRGPAh2CINQet/RrM6Ef196xI20w==" }, "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "PNj+/e/GCJh3ZNzxEGhkMpKJgmmbuGar6Uk/R3mPFZacTx6lBdLs4Ev7uf4XQWqTdJe56rK+2P3oF/9jIGbxgw==", + "resolved": "6.0.25", + "contentHash": "Cmhq0sgb53+dh9xHOlBEQUhi13vsZeQ4fcYC9JYO4med7pabj9x3100opCdUv+7UX+tUC1GPm/nco+1skJdLFA==", "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "6.0.22", - "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.22", - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.22" + "Microsoft.EntityFrameworkCore.Relational": "6.0.25", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.25", + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.25" } }, "Microsoft.Extensions.FileProviders.Abstractions": { @@ -559,8 +559,8 @@ }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -570,10 +570,10 @@ }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } @@ -1816,7 +1816,7 @@ "Monai.Deploy.InformaticsGateway.Database.EntityFramework": "[1.0.0, )", "Monai.Deploy.InformaticsGateway.DicomWeb.Client": "[1.0.0, )", "Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution": "[1.0.0, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Security": "[0.1.3, )", "Monai.Deploy.Storage.MinIO": "[0.2.18, )", "NLog.Web.AspNetCore": "[5.3.4, )", @@ -1826,11 +1826,12 @@ "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { + "HL7-dotnetcore": "[2.36.0, )", "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.22, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.25, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[1.0.4, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging": "[1.0.5, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Storage": "[0.2.18, )", "fo-dicom": "[5.1.1, )" } @@ -1866,7 +1867,7 @@ "type": "Project", "dependencies": { "AspNetCore.HealthChecks.MongoDb": "[6.0.2, )", - "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "[6.0.22, )", + "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "[6.0.25, )", "Microsoft.Extensions.Options.ConfigurationExtensions": "[6.0.0, )", "Monai.Deploy.InformaticsGateway.Api": "[0.4.1, )", "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )", @@ -1886,8 +1887,8 @@ "monai.deploy.informaticsgateway.database.entityframework": { "type": "Project", "dependencies": { - "Microsoft.EntityFrameworkCore": "[6.0.22, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.22, )", + "Microsoft.EntityFrameworkCore": "[6.0.25, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.25, )", "Microsoft.Extensions.Configuration.FileExtensions": "[6.0.0, )", "Microsoft.Extensions.Configuration.Json": "[6.0.0, )", "Monai.Deploy.InformaticsGateway.Api": "[0.4.1, )", @@ -1916,9 +1917,9 @@ "monai.deploy.informaticsgateway.plugins.remoteappexecution": { "type": "Project", "dependencies": { - "Microsoft.EntityFrameworkCore": "[6.0.22, )", - "Microsoft.EntityFrameworkCore.Relational": "[6.0.22, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.22, )", + "Microsoft.EntityFrameworkCore": "[6.0.25, )", + "Microsoft.EntityFrameworkCore.Relational": "[6.0.25, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.25, )", "Microsoft.Extensions.Configuration": "[6.0.1, )", "Microsoft.Extensions.Configuration.FileExtensions": "[6.0.0, )", "Microsoft.Extensions.Configuration.Json": "[6.0.0, )", diff --git a/src/Client/packages.lock.json b/src/Client/packages.lock.json index 8ecf03f09..2a3e704e0 100755 --- a/src/Client/packages.lock.json +++ b/src/Client/packages.lock.json @@ -43,6 +43,11 @@ "System.Threading.Channels": "6.0.0" } }, + "HL7-dotnetcore": { + "type": "Transitive", + "resolved": "2.36.0", + "contentHash": "N1HLMeIqYuY+4O69ItgZJoDBnnpNkK5N2pClceTJ2nFJxsP48iCsA4iz3tm43Yszi4r/vaThoc3UoLBfGP3vKw==" + }, "Macross.Json.Extensions": { "type": "Transitive", "resolved": "3.0.0", @@ -60,8 +65,8 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", @@ -155,8 +160,8 @@ }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -166,10 +171,10 @@ }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } @@ -274,11 +279,12 @@ "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { + "HL7-dotnetcore": "[2.36.0, )", "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.22, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.25, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[1.0.4, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging": "[1.0.5, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Storage": "[0.2.18, )", "fo-dicom": "[5.1.1, )" } diff --git a/src/Configuration/MessageBrokerConfigurationKeys.cs b/src/Configuration/MessageBrokerConfigurationKeys.cs index 846a24c5f..54c4da5a3 100755 --- a/src/Configuration/MessageBrokerConfigurationKeys.cs +++ b/src/Configuration/MessageBrokerConfigurationKeys.cs @@ -47,5 +47,27 @@ public class MessageBrokerConfigurationKeys /// [ConfigurationKeyName("artifactrecieved")] public string ArtifactRecieved { get; set; } = "md.workflow.artifactrecieved"; + + + /// + /// Gets or sets the topic for publishing export requests. + /// Defaults to `md_export_request`. + /// + [ConfigurationKeyName("externalAppRequest")] + public string ExternalAppRequest { get; set; } = "md.externalapp.request"; + + /// + /// Gets or sets the topic for publishing workflow requests. + /// Defaults to `md.export.request`. + /// + [ConfigurationKeyName("exportHl7")] + public string ExportHL7 { get; set; } = "md.export.hl7"; + + /// + /// Gets or sets the topic for publishing export complete requests. + /// Defaults to `md_export_complete`. + /// + [ConfigurationKeyName("exportHl7Complete")] + public string ExportHl7Complete { get; set; } = "md.export.hl7complete"; } } diff --git a/src/Configuration/ScpConfiguration.cs b/src/Configuration/ScpConfiguration.cs old mode 100644 new mode 100755 index 6b66626ee..bac9a2952 --- a/src/Configuration/ScpConfiguration.cs +++ b/src/Configuration/ScpConfiguration.cs @@ -34,6 +34,12 @@ public class ScpConfiguration [ConfigurationKeyName("port")] public int Port { get; set; } = 104; + /// + /// Gets or sets Port number to be used for SCP service. + /// + [ConfigurationKeyName("externalAppPort")] + public int ExternalAppPort { get; set; } = 105; + /// /// Gets or sets maximum number of simultaneous DICOM associations for the SCP service. /// diff --git a/src/Configuration/Test/ValidationExtensionsTest.cs b/src/Configuration/Test/ValidationExtensionsTest.cs old mode 100644 new mode 100755 index 986922e4a..9e00e5dbb --- a/src/Configuration/Test/ValidationExtensionsTest.cs +++ b/src/Configuration/Test/ValidationExtensionsTest.cs @@ -18,6 +18,7 @@ using System.Collections.Generic; using FellowOakDicom; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Xunit; namespace Monai.Deploy.InformaticsGateway.Configuration.Test diff --git a/src/Configuration/Test/packages.lock.json b/src/Configuration/Test/packages.lock.json index d87a39e6c..02e67b1eb 100755 --- a/src/Configuration/Test/packages.lock.json +++ b/src/Configuration/Test/packages.lock.json @@ -102,6 +102,11 @@ "System.Threading.Channels": "6.0.0" } }, + "HL7-dotnetcore": { + "type": "Transitive", + "resolved": "2.36.0", + "contentHash": "N1HLMeIqYuY+4O69ItgZJoDBnnpNkK5N2pClceTJ2nFJxsP48iCsA4iz3tm43Yszi4r/vaThoc3UoLBfGP3vKw==" + }, "Macross.Json.Extensions": { "type": "Transitive", "resolved": "3.0.0", @@ -124,8 +129,8 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", @@ -257,8 +262,8 @@ }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -268,10 +273,10 @@ }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } @@ -1290,11 +1295,12 @@ "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { + "HL7-dotnetcore": "[2.36.0, )", "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.22, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.25, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[1.0.4, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging": "[1.0.5, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Storage": "[0.2.18, )", "fo-dicom": "[5.1.1, )" } diff --git a/src/Configuration/ValidationExtensions.cs b/src/Configuration/ValidationExtensions.cs index da29ce249..09e7bc932 100755 --- a/src/Configuration/ValidationExtensions.cs +++ b/src/Configuration/ValidationExtensions.cs @@ -22,6 +22,7 @@ using Ardalis.GuardClauses; using FellowOakDicom; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; namespace Monai.Deploy.InformaticsGateway.Configuration { @@ -57,6 +58,21 @@ public static bool IsValid(this DestinationApplicationEntity destinationApplicat return valid; } + public static bool IsValid(this HL7DestinationEntity hl7destinationEntity, out IList validationErrors) + { + Guard.Against.Null(hl7destinationEntity, nameof(hl7destinationEntity)); + + validationErrors = new List(); + + var valid = true; + valid &= !string.IsNullOrWhiteSpace(hl7destinationEntity.Name); + valid &= IsAeTitleValid(hl7destinationEntity.GetType().Name, hl7destinationEntity.AeTitle, validationErrors); + valid &= IsValidHostNameIp(hl7destinationEntity.AeTitle, hl7destinationEntity.HostIp, validationErrors); + valid &= IsPortValid(hl7destinationEntity.GetType().Name, hl7destinationEntity.Port, validationErrors); + + return valid; + } + public static bool IsValid(this SourceApplicationEntity sourceApplicationEntity, out IList validationErrors) { Guard.Against.Null(sourceApplicationEntity, nameof(sourceApplicationEntity)); diff --git a/src/Configuration/packages.lock.json b/src/Configuration/packages.lock.json index a05767d6a..2b5691080 100755 --- a/src/Configuration/packages.lock.json +++ b/src/Configuration/packages.lock.json @@ -43,6 +43,11 @@ "System.Threading.Channels": "6.0.0" } }, + "HL7-dotnetcore": { + "type": "Transitive", + "resolved": "2.36.0", + "contentHash": "N1HLMeIqYuY+4O69ItgZJoDBnnpNkK5N2pClceTJ2nFJxsP48iCsA4iz3tm43Yszi4r/vaThoc3UoLBfGP3vKw==" + }, "Macross.Json.Extensions": { "type": "Transitive", "resolved": "3.0.0", @@ -60,8 +65,8 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", @@ -155,8 +160,8 @@ }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -166,10 +171,10 @@ }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } @@ -274,11 +279,12 @@ "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { + "HL7-dotnetcore": "[2.36.0, )", "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.22, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.25, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[1.0.4, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging": "[1.0.5, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Storage": "[0.2.18, )", "fo-dicom": "[5.1.1, )" } diff --git a/src/Database/Api/Repositories/IDestinationApplicationEntityRepository.cs b/src/Database/Api/Repositories/IDestinationApplicationEntityRepository.cs old mode 100644 new mode 100755 index ffc58fa17..dd84793fa --- a/src/Database/Api/Repositories/IDestinationApplicationEntityRepository.cs +++ b/src/Database/Api/Repositories/IDestinationApplicationEntityRepository.cs @@ -15,7 +15,7 @@ */ using System.Linq.Expressions; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; namespace Monai.Deploy.InformaticsGateway.Database.Api.Repositories { diff --git a/src/Database/Api/Repositories/IDicomAssociationInfoRepository.cs b/src/Database/Api/Repositories/IDicomAssociationInfoRepository.cs old mode 100644 new mode 100755 index 2457dcb7e..796ff43bc --- a/src/Database/Api/Repositories/IDicomAssociationInfoRepository.cs +++ b/src/Database/Api/Repositories/IDicomAssociationInfoRepository.cs @@ -14,7 +14,7 @@ * limitations under the License. */ -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; namespace Monai.Deploy.InformaticsGateway.Database.Api.Repositories { diff --git a/src/Database/Api/Repositories/IExternalAppDeatilsRepository.cs b/src/Database/Api/Repositories/IExternalAppDeatilsRepository.cs new file mode 100755 index 000000000..51668aefd --- /dev/null +++ b/src/Database/Api/Repositories/IExternalAppDeatilsRepository.cs @@ -0,0 +1,31 @@ +/* + * Copyright 2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 Monai.Deploy.InformaticsGateway.Api.Models; + +namespace Monai.Deploy.InformaticsGateway.Database.Api.Repositories +{ + public interface IExternalAppDetailsRepository + { + Task AddAsync(ExternalAppDetails details, CancellationToken cancellationToken); + + Task> GetAsync(string studyInstanceId, CancellationToken cancellationToken); + + Task GetByPatientIdOutboundAsync(string patientId, CancellationToken cancellationToken); + + Task GetByStudyIdOutboundAsync(string studyInstanceId, CancellationToken cancellationToken); + } +} diff --git a/src/Database/Api/Repositories/IHL7DestinationEntityRepository.cs b/src/Database/Api/Repositories/IHL7DestinationEntityRepository.cs new file mode 100644 index 000000000..c46dc3503 --- /dev/null +++ b/src/Database/Api/Repositories/IHL7DestinationEntityRepository.cs @@ -0,0 +1,36 @@ +/* + * Copyright 2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.Linq.Expressions; +using Monai.Deploy.InformaticsGateway.Api.Models; + +namespace Monai.Deploy.InformaticsGateway.Database.Api.Repositories +{ + public interface IHL7DestinationEntityRepository + { + Task> ToListAsync(CancellationToken cancellationToken = default); + + Task FindByNameAsync(string name, CancellationToken cancellationToken = default); + + Task AddAsync(HL7DestinationEntity item, CancellationToken cancellationToken = default); + + Task UpdateAsync(HL7DestinationEntity entity, CancellationToken cancellationToken = default); + + Task RemoveAsync(HL7DestinationEntity entity, CancellationToken cancellationToken = default); + + Task ContainsAsync(Expression> predicate, CancellationToken cancellationToken = default); + } +} diff --git a/src/Database/Api/Repositories/IHl7ApplicationConfigRepository.cs b/src/Database/Api/Repositories/IHl7ApplicationConfigRepository.cs new file mode 100644 index 000000000..b381d4da6 --- /dev/null +++ b/src/Database/Api/Repositories/IHl7ApplicationConfigRepository.cs @@ -0,0 +1,35 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 Monai.Deploy.InformaticsGateway.Api; + +namespace Monai.Deploy.InformaticsGateway.Database.Api.Repositories +{ + public interface IHl7ApplicationConfigRepository + { + Task> GetAllAsync(CancellationToken cancellationToken = default); + + Task GetByIdAsync(string id); + + Task DeleteAsync(string id, CancellationToken cancellationToken = default); + + Task CreateAsync(Hl7ApplicationConfigEntity configEntity, + CancellationToken cancellationToken = default); + + Task UpdateAsync(Hl7ApplicationConfigEntity configEntity, + CancellationToken cancellationToken = default); + } +} diff --git a/src/Database/Api/Repositories/IMonaiApplicationEntityRepository.cs b/src/Database/Api/Repositories/IMonaiApplicationEntityRepository.cs old mode 100644 new mode 100755 index 1b803385e..ccfbd6567 --- a/src/Database/Api/Repositories/IMonaiApplicationEntityRepository.cs +++ b/src/Database/Api/Repositories/IMonaiApplicationEntityRepository.cs @@ -15,7 +15,7 @@ */ using System.Linq.Expressions; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; namespace Monai.Deploy.InformaticsGateway.Database.Api.Repositories { diff --git a/src/Database/Api/Repositories/InferenceRequestRepositoryBase.cs b/src/Database/Api/Repositories/InferenceRequestRepositoryBase.cs index 157f703b8..8775eb688 100755 --- a/src/Database/Api/Repositories/InferenceRequestRepositoryBase.cs +++ b/src/Database/Api/Repositories/InferenceRequestRepositoryBase.cs @@ -17,7 +17,6 @@ using Ardalis.GuardClauses; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; using Monai.Deploy.InformaticsGateway.Api.Rest; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.Api.Logging; @@ -63,7 +62,7 @@ public async Task UpdateAsync(InferenceRequest inferenceRequest, InferenceReques { Guard.Against.Null(inferenceRequest, nameof(inferenceRequest)); - using var loggerScope = _logger.BeginScope(new LoggingDataDictionary { { "TransactionId", inferenceRequest.TransactionId } }); + using var loggerScope = _logger.BeginScope(new InformaticsGateway.Api.LoggingDataDictionary { { "TransactionId", inferenceRequest.TransactionId } }); if (status == InferenceRequestStatus.Success) { diff --git a/src/Database/Api/StorageMetadataWrapper.cs b/src/Database/Api/StorageMetadataWrapper.cs index 326cce2cf..dac5fb729 100755 --- a/src/Database/Api/StorageMetadataWrapper.cs +++ b/src/Database/Api/StorageMetadataWrapper.cs @@ -17,7 +17,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using Ardalis.GuardClauses; -using Monai.Deploy.InformaticsGateway.Api; using Monai.Deploy.InformaticsGateway.Api.Storage; namespace Monai.Deploy.InformaticsGateway.Database.Api diff --git a/src/Database/Api/Test/packages.lock.json b/src/Database/Api/Test/packages.lock.json index dd4d34036..65aff269f 100755 --- a/src/Database/Api/Test/packages.lock.json +++ b/src/Database/Api/Test/packages.lock.json @@ -76,6 +76,11 @@ "System.Threading.Channels": "6.0.0" } }, + "HL7-dotnetcore": { + "type": "Transitive", + "resolved": "2.36.0", + "contentHash": "N1HLMeIqYuY+4O69ItgZJoDBnnpNkK5N2pClceTJ2nFJxsP48iCsA4iz3tm43Yszi4r/vaThoc3UoLBfGP3vKw==" + }, "Macross.Json.Extensions": { "type": "Transitive", "resolved": "3.0.0", @@ -98,8 +103,8 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", @@ -231,8 +236,8 @@ }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -242,10 +247,10 @@ }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } @@ -1264,11 +1269,12 @@ "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { + "HL7-dotnetcore": "[2.36.0, )", "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.22, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.25, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[1.0.4, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging": "[1.0.5, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Storage": "[0.2.18, )", "fo-dicom": "[5.1.1, )" } diff --git a/src/Database/Api/packages.lock.json b/src/Database/Api/packages.lock.json index 9e1f1f4de..53e07aa40 100755 --- a/src/Database/Api/packages.lock.json +++ b/src/Database/Api/packages.lock.json @@ -49,6 +49,11 @@ "System.Threading.Channels": "6.0.0" } }, + "HL7-dotnetcore": { + "type": "Transitive", + "resolved": "2.36.0", + "contentHash": "N1HLMeIqYuY+4O69ItgZJoDBnnpNkK5N2pClceTJ2nFJxsP48iCsA4iz3tm43Yszi4r/vaThoc3UoLBfGP3vKw==" + }, "Macross.Json.Extensions": { "type": "Transitive", "resolved": "3.0.0", @@ -66,8 +71,8 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", @@ -161,8 +166,8 @@ }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -172,10 +177,10 @@ }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } @@ -280,11 +285,12 @@ "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { + "HL7-dotnetcore": "[2.36.0, )", "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.22, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.25, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[1.0.4, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging": "[1.0.5, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Storage": "[0.2.18, )", "fo-dicom": "[5.1.1, )" } diff --git a/src/Database/DatabaseManager.cs b/src/Database/DatabaseManager.cs index 2efe60d8d..cb40c9a0d 100755 --- a/src/Database/DatabaseManager.cs +++ b/src/Database/DatabaseManager.cs @@ -84,6 +84,7 @@ public static IServiceCollection ConfigureDatabase(this IServiceCollection servi ServiceLifetime.Transient); services.AddScoped(); services.AddScoped(typeof(IDestinationApplicationEntityRepository), typeof(EntityFramework.Repositories.DestinationApplicationEntityRepository)); + services.AddScoped(typeof(IHL7DestinationEntityRepository), typeof(EntityFramework.Repositories.HL7DestinationEntityRepository)); services.AddScoped(typeof(IInferenceRequestRepository), typeof(EntityFramework.Repositories.InferenceRequestRepository)); services.AddScoped(typeof(IMonaiApplicationEntityRepository), typeof(EntityFramework.Repositories.MonaiApplicationEntityRepository)); services.AddScoped(typeof(ISourceApplicationEntityRepository), typeof(EntityFramework.Repositories.SourceApplicationEntityRepository)); @@ -91,6 +92,8 @@ public static IServiceCollection ConfigureDatabase(this IServiceCollection servi services.AddScoped(typeof(IPayloadRepository), typeof(EntityFramework.Repositories.PayloadRepository)); services.AddScoped(typeof(IDicomAssociationInfoRepository), typeof(EntityFramework.Repositories.DicomAssociationInfoRepository)); services.AddScoped(typeof(IVirtualApplicationEntityRepository), typeof(EntityFramework.Repositories.VirtualApplicationEntityRepository)); + services.AddScoped(typeof(IHl7ApplicationConfigRepository), typeof(EntityFramework.Repositories.Hl7ApplicationConfigRepository)); + services.AddSingleton(typeof(IExternalAppDetailsRepository), typeof(EntityFramework.Repositories.ExternalAppDetailsRepository)); services.ConfigureDatabaseFromPlugIns(DatabaseType.EntityFramework, fileSystem, connectionStringConfigurationSection, pluginsConfigurationSection, loggerFactory); return services; @@ -99,6 +102,7 @@ public static IServiceCollection ConfigureDatabase(this IServiceCollection servi services.AddSingleton(s => new MongoClient(connectionStringConfigurationSection[SR.DatabaseConnectionStringKey])); services.AddScoped(); services.AddScoped(typeof(IDestinationApplicationEntityRepository), typeof(MongoDB.Repositories.DestinationApplicationEntityRepository)); + services.AddScoped(typeof(IHL7DestinationEntityRepository), typeof(MongoDB.Repositories.HL7DestinationEntityRepository)); services.AddScoped(typeof(IInferenceRequestRepository), typeof(MongoDB.Repositories.InferenceRequestRepository)); services.AddScoped(typeof(IMonaiApplicationEntityRepository), typeof(MongoDB.Repositories.MonaiApplicationEntityRepository)); services.AddScoped(typeof(ISourceApplicationEntityRepository), typeof(MongoDB.Repositories.SourceApplicationEntityRepository)); @@ -106,6 +110,8 @@ public static IServiceCollection ConfigureDatabase(this IServiceCollection servi services.AddScoped(typeof(IPayloadRepository), typeof(MongoDB.Repositories.PayloadRepository)); services.AddScoped(typeof(IDicomAssociationInfoRepository), typeof(MongoDB.Repositories.DicomAssociationInfoRepository)); services.AddScoped(typeof(IVirtualApplicationEntityRepository), typeof(MongoDB.Repositories.VirtualApplicationEntityRepository)); + services.AddScoped(typeof(IHl7ApplicationConfigRepository), typeof(MongoDB.Repositories.Hl7ApplicationConfigRepository)); + services.AddSingleton(typeof(IExternalAppDetailsRepository), typeof(MongoDB.Repositories.ExternalAppDetailsRepository)); services.ConfigureDatabaseFromPlugIns(DatabaseType.MongoDb, fileSystem, connectionStringConfigurationSection, pluginsConfigurationSection, loggerFactory); diff --git a/src/Database/DatabaseMigrationManager.cs b/src/Database/DatabaseMigrationManager.cs old mode 100644 new mode 100755 index da35c17a4..72acb2cd8 --- a/src/Database/DatabaseMigrationManager.cs +++ b/src/Database/DatabaseMigrationManager.cs @@ -61,7 +61,7 @@ private static Type[] FindMatchingTypesFromAssemblies(Assembly[] assemblies) var matchingTypes = new List(); foreach (var assembly in assemblies) { - var types = assembly.ExportedTypes.Where(p => p.IsAssignableFrom(typeof(IDatabaseMigrationManager))); + var types = assembly.ExportedTypes.Where(p => p.IsAssignableFrom(typeof(IDatabaseMigrationManager)) && p.Name != nameof(IDatabaseMigrationManager)); if (types.Any()) { matchingTypes.AddRange(types); diff --git a/src/Database/EntityFramework/Configuration/DestinationApplicationEntityConfiguration.cs b/src/Database/EntityFramework/Configuration/DestinationApplicationEntityConfiguration.cs old mode 100644 new mode 100755 index ad0688719..33d7ff2f5 --- a/src/Database/EntityFramework/Configuration/DestinationApplicationEntityConfiguration.cs +++ b/src/Database/EntityFramework/Configuration/DestinationApplicationEntityConfiguration.cs @@ -17,7 +17,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Configuration { diff --git a/src/Database/EntityFramework/Configuration/DicomAssociationInfoConfiguration.cs b/src/Database/EntityFramework/Configuration/DicomAssociationInfoConfiguration.cs index 67bda9b8d..d8b2c45f5 100755 --- a/src/Database/EntityFramework/Configuration/DicomAssociationInfoConfiguration.cs +++ b/src/Database/EntityFramework/Configuration/DicomAssociationInfoConfiguration.cs @@ -20,7 +20,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Configuration { diff --git a/src/Database/EntityFramework/Configuration/ExternalAppDetailsConfiguration.cs b/src/Database/EntityFramework/Configuration/ExternalAppDetailsConfiguration.cs new file mode 100755 index 000000000..cfdcf8d9d --- /dev/null +++ b/src/Database/EntityFramework/Configuration/ExternalAppDetailsConfiguration.cs @@ -0,0 +1,44 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.Text.Json.Serialization; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Monai.Deploy.InformaticsGateway.Api.Models; + +namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Configuration +{ + internal class ExternalAppDetailsConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + var jsonSerializerSettings = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + builder.HasKey(j => j.Id); + + builder.Property(j => j.StudyInstanceUid).IsRequired(); + builder.Property(j => j.WorkflowInstanceId).IsRequired(); + builder.Property(j => j.DateTimeCreated).IsRequired(); + builder.Property(j => j.CorrelationId).IsRequired(); + builder.Property(j => j.ExportTaskID).IsRequired(); + } + } +} diff --git a/src/Database/EntityFramework/Configuration/HL7DestinationEntityConfiguration.cs b/src/Database/EntityFramework/Configuration/HL7DestinationEntityConfiguration.cs new file mode 100644 index 000000000..703dad6f3 --- /dev/null +++ b/src/Database/EntityFramework/Configuration/HL7DestinationEntityConfiguration.cs @@ -0,0 +1,43 @@ +/* + * Copyright 2021-2022 MONAI Consortium + * Copyright 2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Monai.Deploy.InformaticsGateway.Api.Models; + +namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Configuration +{ + internal class HL7DestinationEntityConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(j => j.Name); + builder.Property(j => j.AeTitle).IsRequired(); + builder.Property(j => j.Port).IsRequired(); + builder.Property(j => j.HostIp).IsRequired(); + builder.Property(j => j.CreatedBy).IsRequired(false); + builder.Property(j => j.UpdatedBy).IsRequired(false); + builder.Property(j => j.DateTimeCreated).IsRequired(); + builder.Property(j => j.DateTimeUpdated).IsRequired(false); + + builder.HasIndex(p => p.Name, "idx_destination_name").IsUnique(); + builder.HasIndex(p => new { p.Name, p.AeTitle, p.HostIp, p.Port }, "idx_source_all").IsUnique(); + + builder.Ignore(p => p.Id); + } + } +} diff --git a/src/Database/EntityFramework/Configuration/Hl7ApplicationConfigConfiguration.cs b/src/Database/EntityFramework/Configuration/Hl7ApplicationConfigConfiguration.cs new file mode 100755 index 000000000..cfd9a186b --- /dev/null +++ b/src/Database/EntityFramework/Configuration/Hl7ApplicationConfigConfiguration.cs @@ -0,0 +1,86 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.Text.Json.Serialization; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Monai.Deploy.InformaticsGateway.Api; + +namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Configuration +{ + internal class Hl7ApplicationConfigConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + var valueComparer = new ValueComparer>( + (c1, c2) => c1!.SequenceEqual(c2!), + c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), + c => c.ToList()); + + var jsonSerializerSettings = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + builder.HasKey(j => j.Name); + builder.Property(j => j.SendingId).HasConversion( + v => JsonSerializer.Serialize(v, jsonSerializerSettings), + v => JsonSerializer.Deserialize(v, jsonSerializerSettings)!) + .IsRequired() + .Metadata + .SetValueComparer( + new ValueComparer( + (c1, c2) => c1 == c2, + c => c.GetHashCode(), + c => c)); + + builder.Property(j => j.DataLink).HasConversion( + v => JsonSerializer.Serialize(v, jsonSerializerSettings), + v => JsonSerializer.Deserialize(v, jsonSerializerSettings)!) + .IsRequired() + .Metadata + .SetValueComparer( + new ValueComparer( + (c1, c2) => c1 == c2, + c => c.GetHashCode(), + c => c)); + + builder.Property(j => j.DateTimeCreated).IsRequired(); + builder.Property(j => j.PlugInAssemblies) + .HasConversion( + v => JsonSerializer.Serialize(v, jsonSerializerSettings), + v => JsonSerializer.Deserialize>(v, jsonSerializerSettings)!) + .Metadata.SetValueComparer(valueComparer); + + builder.Property(j => j.DataMapping) + .HasConversion( + v => JsonSerializer.Serialize(v, jsonSerializerSettings), + v => JsonSerializer.Deserialize>(v, jsonSerializerSettings)!) + .Metadata + .SetValueComparer( + new ValueComparer>( + (c1, c2) => c1!.SequenceEqual(c2!), + c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), + c => c.ToList())); + + builder.HasIndex(p => p.Name, "idx_hl7_name").IsUnique(); + + builder.Ignore(p => p.Id); + } + } +} diff --git a/src/Database/EntityFramework/Configuration/MonaiApplicationEntityConfiguration.cs b/src/Database/EntityFramework/Configuration/MonaiApplicationEntityConfiguration.cs old mode 100644 new mode 100755 index 3e78d3ad9..fc9f1666c --- a/src/Database/EntityFramework/Configuration/MonaiApplicationEntityConfiguration.cs +++ b/src/Database/EntityFramework/Configuration/MonaiApplicationEntityConfiguration.cs @@ -20,7 +20,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Configuration { diff --git a/src/Database/EntityFramework/InformaticsGatewayContext.cs b/src/Database/EntityFramework/InformaticsGatewayContext.cs index 52b410c39..40af12bde 100755 --- a/src/Database/EntityFramework/InformaticsGatewayContext.cs +++ b/src/Database/EntityFramework/InformaticsGatewayContext.cs @@ -18,6 +18,7 @@ using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Api.Rest; using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Database.Api; @@ -36,11 +37,15 @@ public InformaticsGatewayContext(DbContextOptions opt public virtual DbSet MonaiApplicationEntities { get; set; } public virtual DbSet SourceApplicationEntities { get; set; } public virtual DbSet DestinationApplicationEntities { get; set; } + public virtual DbSet HL7DestinationEntities { get; set; } public virtual DbSet InferenceRequests { get; set; } public virtual DbSet Payloads { get; set; } public virtual DbSet StorageMetadataWrapperEntities { get; set; } public virtual DbSet DicomAssociationHistories { get; set; } public virtual DbSet VirtualApplicationEntities { get; set; } + public virtual DbSet ExternalAppDetails { get; set; } + + public virtual DbSet Hl7ApplicationConfig { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -49,11 +54,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfiguration(new MonaiApplicationEntityConfiguration()); modelBuilder.ApplyConfiguration(new SourceApplicationEntityConfiguration()); modelBuilder.ApplyConfiguration(new DestinationApplicationEntityConfiguration()); + modelBuilder.ApplyConfiguration(new HL7DestinationEntityConfiguration()); modelBuilder.ApplyConfiguration(new InferenceRequestConfiguration()); modelBuilder.ApplyConfiguration(new PayloadConfiguration()); modelBuilder.ApplyConfiguration(new StorageMetadataWrapperEntityConfiguration()); modelBuilder.ApplyConfiguration(new DicomAssociationInfoConfiguration()); modelBuilder.ApplyConfiguration(new VirtualApplicationEntityConfiguration()); + modelBuilder.ApplyConfiguration(new Hl7ApplicationConfigConfiguration()); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) diff --git a/src/Database/EntityFramework/Migrations/20231120161347_202311201611.Designer.cs b/src/Database/EntityFramework/Migrations/20231120161347_202311201611.Designer.cs new file mode 100755 index 000000000..cecea913d --- /dev/null +++ b/src/Database/EntityFramework/Migrations/20231120161347_202311201611.Designer.cs @@ -0,0 +1,431 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Monai.Deploy.InformaticsGateway.Database.EntityFramework; + +#nullable disable + +namespace Monai.Deploy.InformaticsGateway.Database.Migrations +{ + [DbContext(typeof(InformaticsGatewayContext))] + [Migration("20231120161347_202311201611")] + partial class _202311201611 + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.22"); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Models.DestinationApplicationEntity", b => + { + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("AeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DateTimeUpdated") + .HasColumnType("TEXT"); + + b.Property("HostIp") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.HasIndex(new[] { "Name" }, "idx_destination_name") + .IsUnique(); + + b.HasIndex(new[] { "Name", "AeTitle", "HostIp", "Port" }, "idx_source_all") + .IsUnique(); + + b.ToTable("DestinationApplicationEntities"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Models.DicomAssociationInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CalledAeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CallingAeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DateTimeDisconnected") + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("Errors") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("PayloadIds") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RemoteHost") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RemotePort") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("DicomAssociationHistories"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Models.ExternalAppDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DestinationFolder") + .HasColumnType("TEXT"); + + b.Property("ExportTaskID") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PatientId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PatientIdOutBound") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StudyInstanceUid") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StudyInstanceUidOutBound") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ExternalAppDetails"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Models.MonaiApplicationEntity", b => + { + b.Property("Name") + .HasColumnType("TEXT") + .HasColumnOrder(0); + + b.Property("AeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AllowedSopClasses") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DateTimeUpdated") + .HasColumnType("TEXT"); + + b.Property("Grouping") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IgnoredSopClasses") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlugInAssemblies") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Timeout") + .HasColumnType("INTEGER"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("Workflows") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.HasIndex(new[] { "Name" }, "idx_monaiae_name") + .IsUnique(); + + b.ToTable("MonaiApplicationEntities"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Rest.InferenceRequest", b => + { + b.Property("InferenceRequestId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("InputMetadata") + .HasColumnType("TEXT"); + + b.Property("InputResources") + .HasColumnType("TEXT"); + + b.Property("OutputResources") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("TransactionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TryCount") + .HasColumnType("INTEGER"); + + b.HasKey("InferenceRequestId"); + + b.HasIndex(new[] { "InferenceRequestId" }, "idx_inferencerequest_inferencerequestid") + .IsUnique(); + + b.HasIndex(new[] { "State" }, "idx_inferencerequest_state"); + + b.HasIndex(new[] { "TransactionId" }, "idx_inferencerequest_transactionid") + .IsUnique(); + + b.ToTable("InferenceRequests"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.SourceApplicationEntity", b => + { + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("AeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DateTimeUpdated") + .HasColumnType("TEXT"); + + b.Property("HostIp") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.HasIndex(new[] { "Name", "AeTitle", "HostIp" }, "idx_source_all") + .IsUnique() + .HasDatabaseName("idx_source_all1"); + + b.HasIndex(new[] { "Name" }, "idx_source_name") + .IsUnique(); + + b.ToTable("SourceApplicationEntities"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Storage.Payload", b => + { + b.Property("PayloadId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DataOrigins") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DataTrigger") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DestinationFolder") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Files") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MachineName") + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.Property("TaskId") + .HasColumnType("TEXT"); + + b.Property("Timeout") + .HasColumnType("INTEGER"); + + b.Property("WorkflowInstanceId") + .HasColumnType("TEXT"); + + b.HasKey("PayloadId"); + + b.HasIndex(new[] { "CorrelationId", "PayloadId" }, "idx_payload_ids") + .IsUnique(); + + b.HasIndex(new[] { "State" }, "idx_payload_state"); + + b.ToTable("Payloads"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.VirtualApplicationEntity", b => + { + b.Property("Name") + .HasColumnType("TEXT") + .HasColumnOrder(0); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DateTimeUpdated") + .HasColumnType("TEXT"); + + b.Property("PlugInAssemblies") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("VirtualAeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Workflows") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.HasIndex(new[] { "Name" }, "idx_virtualae_name") + .IsUnique(); + + b.ToTable("VirtualApplicationEntities"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Database.Api.StorageMetadataWrapper", b => + { + b.Property("CorrelationId") + .HasColumnType("TEXT"); + + b.Property("Identity") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("IsUploaded") + .HasColumnType("INTEGER"); + + b.Property("TypeName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("CorrelationId", "Identity"); + + b.HasIndex(new[] { "CorrelationId" }, "idx_storagemetadata_correlation"); + + b.HasIndex(new[] { "CorrelationId", "Identity" }, "idx_storagemetadata_ids"); + + b.HasIndex(new[] { "IsUploaded" }, "idx_storagemetadata_uploaded"); + + b.ToTable("StorageMetadataWrapperEntities"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Database/EntityFramework/Migrations/20231120161347_202311201611.cs b/src/Database/EntityFramework/Migrations/20231120161347_202311201611.cs new file mode 100755 index 000000000..182e05f68 --- /dev/null +++ b/src/Database/EntityFramework/Migrations/20231120161347_202311201611.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Monai.Deploy.InformaticsGateway.Database.Migrations +{ + public partial class _202311201611 : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DestinationFolder", + table: "Payloads", + type: "TEXT", + nullable: false, + defaultValue: ""); + + migrationBuilder.CreateTable( + name: "ExternalAppDetails", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + StudyInstanceUid = table.Column(type: "TEXT", nullable: false), + StudyInstanceUidOutBound = table.Column(type: "TEXT", nullable: false), + WorkflowInstanceId = table.Column(type: "TEXT", nullable: false), + ExportTaskID = table.Column(type: "TEXT", nullable: false), + CorrelationId = table.Column(type: "TEXT", nullable: false), + DestinationFolder = table.Column(type: "TEXT", nullable: true), + PatientId = table.Column(type: "TEXT", nullable: false), + PatientIdOutBound = table.Column(type: "TEXT", nullable: false), + DateTimeCreated = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExternalAppDetails", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ExternalAppDetails"); + + migrationBuilder.DropColumn( + name: "DestinationFolder", + table: "Payloads"); + } + } +} diff --git a/src/Database/EntityFramework/Migrations/20231204113501_Hl7DEstinationAndConfig.Designer.cs b/src/Database/EntityFramework/Migrations/20231204113501_Hl7DEstinationAndConfig.Designer.cs new file mode 100755 index 000000000..742aec068 --- /dev/null +++ b/src/Database/EntityFramework/Migrations/20231204113501_Hl7DEstinationAndConfig.Designer.cs @@ -0,0 +1,505 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Monai.Deploy.InformaticsGateway.Database.EntityFramework; + +#nullable disable + +namespace Monai.Deploy.InformaticsGateway.Database.Migrations +{ + [DbContext(typeof(InformaticsGatewayContext))] + [Migration("20231204113501_Hl7DEstinationAndConfig")] + partial class Hl7DEstinationAndConfig + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.25"); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Hl7ApplicationConfigEntity", b => + { + b.Property("Name") + .HasColumnType("TEXT") + .HasColumnOrder(0); + + b.Property("DataLink") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DataMapping") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("PlugInAssemblies") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SendingId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.HasIndex(new[] { "Name" }, "idx_hl7_name") + .IsUnique(); + + b.ToTable("Hl7ApplicationConfig"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Models.DestinationApplicationEntity", b => + { + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("AeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DateTimeUpdated") + .HasColumnType("TEXT"); + + b.Property("HostIp") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.HasIndex(new[] { "Name" }, "idx_destination_name") + .IsUnique(); + + b.HasIndex(new[] { "Name", "AeTitle", "HostIp", "Port" }, "idx_source_all") + .IsUnique(); + + b.ToTable("DestinationApplicationEntities"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Models.DicomAssociationInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CalledAeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CallingAeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DateTimeDisconnected") + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("Errors") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("PayloadIds") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RemoteHost") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RemotePort") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("DicomAssociationHistories"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Models.ExternalAppDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DestinationFolder") + .HasColumnType("TEXT"); + + b.Property("ExportTaskID") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PatientId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PatientIdOutBound") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StudyInstanceUid") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StudyInstanceUidOutBound") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ExternalAppDetails"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Models.HL7DestinationEntity", b => + { + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("AeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DateTimeUpdated") + .HasColumnType("TEXT"); + + b.Property("HostIp") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.HasIndex(new[] { "Name" }, "idx_destination_name") + .IsUnique() + .HasDatabaseName("idx_destination_name1"); + + b.HasIndex(new[] { "Name", "AeTitle", "HostIp", "Port" }, "idx_source_all") + .IsUnique() + .HasDatabaseName("idx_source_all1"); + + b.ToTable("HL7DestinationEntities"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Models.MonaiApplicationEntity", b => + { + b.Property("Name") + .HasColumnType("TEXT") + .HasColumnOrder(0); + + b.Property("AeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AllowedSopClasses") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DateTimeUpdated") + .HasColumnType("TEXT"); + + b.Property("Grouping") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IgnoredSopClasses") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlugInAssemblies") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Timeout") + .HasColumnType("INTEGER"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("Workflows") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.HasIndex(new[] { "Name" }, "idx_monaiae_name") + .IsUnique(); + + b.ToTable("MonaiApplicationEntities"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Rest.InferenceRequest", b => + { + b.Property("InferenceRequestId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("InputMetadata") + .HasColumnType("TEXT"); + + b.Property("InputResources") + .HasColumnType("TEXT"); + + b.Property("OutputResources") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("TransactionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TryCount") + .HasColumnType("INTEGER"); + + b.HasKey("InferenceRequestId"); + + b.HasIndex(new[] { "InferenceRequestId" }, "idx_inferencerequest_inferencerequestid") + .IsUnique(); + + b.HasIndex(new[] { "State" }, "idx_inferencerequest_state"); + + b.HasIndex(new[] { "TransactionId" }, "idx_inferencerequest_transactionid") + .IsUnique(); + + b.ToTable("InferenceRequests"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.SourceApplicationEntity", b => + { + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("AeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DateTimeUpdated") + .HasColumnType("TEXT"); + + b.Property("HostIp") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.HasIndex(new[] { "Name", "AeTitle", "HostIp" }, "idx_source_all") + .IsUnique() + .HasDatabaseName("idx_source_all2"); + + b.HasIndex(new[] { "Name" }, "idx_source_name") + .IsUnique(); + + b.ToTable("SourceApplicationEntities"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Storage.Payload", b => + { + b.Property("PayloadId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DataOrigins") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DataTrigger") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DestinationFolder") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Files") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MachineName") + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.Property("TaskId") + .HasColumnType("TEXT"); + + b.Property("Timeout") + .HasColumnType("INTEGER"); + + b.Property("WorkflowInstanceId") + .HasColumnType("TEXT"); + + b.HasKey("PayloadId"); + + b.HasIndex(new[] { "CorrelationId", "PayloadId" }, "idx_payload_ids") + .IsUnique(); + + b.HasIndex(new[] { "State" }, "idx_payload_state"); + + b.ToTable("Payloads"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.VirtualApplicationEntity", b => + { + b.Property("Name") + .HasColumnType("TEXT") + .HasColumnOrder(0); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DateTimeUpdated") + .HasColumnType("TEXT"); + + b.Property("PlugInAssemblies") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("VirtualAeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Workflows") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.HasIndex(new[] { "Name" }, "idx_virtualae_name") + .IsUnique(); + + b.ToTable("VirtualApplicationEntities"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Database.Api.StorageMetadataWrapper", b => + { + b.Property("CorrelationId") + .HasColumnType("TEXT"); + + b.Property("Identity") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("IsUploaded") + .HasColumnType("INTEGER"); + + b.Property("TypeName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("CorrelationId", "Identity"); + + b.HasIndex(new[] { "CorrelationId" }, "idx_storagemetadata_correlation"); + + b.HasIndex(new[] { "CorrelationId", "Identity" }, "idx_storagemetadata_ids"); + + b.HasIndex(new[] { "IsUploaded" }, "idx_storagemetadata_uploaded"); + + b.ToTable("StorageMetadataWrapperEntities"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Database/EntityFramework/Migrations/20231204113501_Hl7DEstinationAndConfig.cs b/src/Database/EntityFramework/Migrations/20231204113501_Hl7DEstinationAndConfig.cs new file mode 100755 index 000000000..d23d89c28 --- /dev/null +++ b/src/Database/EntityFramework/Migrations/20231204113501_Hl7DEstinationAndConfig.cs @@ -0,0 +1,78 @@ + +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Monai.Deploy.InformaticsGateway.Database.Migrations +{ + public partial class Hl7DEstinationAndConfig : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Hl7ApplicationConfig", + columns: table => new + { + Name = table.Column(type: "TEXT", nullable: false), + SendingId = table.Column(type: "TEXT", nullable: false), + DataLink = table.Column(type: "TEXT", nullable: false), + DataMapping = table.Column(type: "TEXT", nullable: false), + PlugInAssemblies = table.Column(type: "TEXT", nullable: false), + DateTimeCreated = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Hl7ApplicationConfig", x => x.Name); + }); + + migrationBuilder.CreateTable( + name: "HL7DestinationEntities", + columns: table => new + { + Name = table.Column(type: "TEXT", nullable: false), + Port = table.Column(type: "INTEGER", nullable: false), + DateTimeCreated = table.Column(type: "TEXT", nullable: false), + AeTitle = table.Column(type: "TEXT", nullable: false), + HostIp = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true), + DateTimeUpdated = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_HL7DestinationEntities", x => x.Name); + }); + + migrationBuilder.CreateIndex( + name: "idx_hl7_name", + table: "Hl7ApplicationConfig", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx_destination_name1", + table: "HL7DestinationEntities", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx_source_all_HL7Destination", + table: "HL7DestinationEntities", + columns: new[] { "Name", "AeTitle", "HostIp", "Port" }, + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Hl7ApplicationConfig"); + + migrationBuilder.DropTable( + name: "HL7DestinationEntities"); + + migrationBuilder.DropIndex( + name: "idx_source_all_HL7Destination", + table: "HL7DestinationEntities"); + } + } +} diff --git a/src/Database/EntityFramework/Migrations/20231207154732_Hl7Plugins.Designer.cs b/src/Database/EntityFramework/Migrations/20231207154732_Hl7Plugins.Designer.cs new file mode 100755 index 000000000..256eb5527 --- /dev/null +++ b/src/Database/EntityFramework/Migrations/20231207154732_Hl7Plugins.Designer.cs @@ -0,0 +1,508 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Monai.Deploy.InformaticsGateway.Database.EntityFramework; + +#nullable disable + +namespace Monai.Deploy.InformaticsGateway.Database.Migrations +{ + [DbContext(typeof(InformaticsGatewayContext))] + [Migration("20231207154732_Hl7Plugins")] + partial class Hl7Plugins + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.25"); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Hl7ApplicationConfigEntity", b => + { + b.Property("Name") + .HasColumnType("TEXT") + .HasColumnOrder(0); + + b.Property("DataLink") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DataMapping") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("PlugInAssemblies") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SendingId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.HasIndex(new[] { "Name" }, "idx_hl7_name") + .IsUnique(); + + b.ToTable("Hl7ApplicationConfig"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Models.DestinationApplicationEntity", b => + { + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("AeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DateTimeUpdated") + .HasColumnType("TEXT"); + + b.Property("HostIp") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.HasIndex(new[] { "Name" }, "idx_destination_name") + .IsUnique(); + + b.HasIndex(new[] { "Name", "AeTitle", "HostIp", "Port" }, "idx_source_all") + .IsUnique(); + + b.ToTable("DestinationApplicationEntities"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Models.DicomAssociationInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CalledAeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CallingAeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DateTimeDisconnected") + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("Errors") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("PayloadIds") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RemoteHost") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RemotePort") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("DicomAssociationHistories"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Models.ExternalAppDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DestinationFolder") + .HasColumnType("TEXT"); + + b.Property("ExportTaskID") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PatientId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PatientIdOutBound") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StudyInstanceUid") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StudyInstanceUidOutBound") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ExternalAppDetails"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Models.HL7DestinationEntity", b => + { + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("AeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DateTimeUpdated") + .HasColumnType("TEXT"); + + b.Property("HostIp") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.HasIndex(new[] { "Name" }, "idx_destination_name") + .IsUnique() + .HasDatabaseName("idx_destination_name1"); + + b.HasIndex(new[] { "Name", "AeTitle", "HostIp", "Port" }, "idx_source_all") + .IsUnique() + .HasDatabaseName("idx_source_all1"); + + b.ToTable("HL7DestinationEntities"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Models.MonaiApplicationEntity", b => + { + b.Property("Name") + .HasColumnType("TEXT") + .HasColumnOrder(0); + + b.Property("AeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AllowedSopClasses") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DateTimeUpdated") + .HasColumnType("TEXT"); + + b.Property("Grouping") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IgnoredSopClasses") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlugInAssemblies") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Timeout") + .HasColumnType("INTEGER"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("Workflows") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.HasIndex(new[] { "Name" }, "idx_monaiae_name") + .IsUnique(); + + b.ToTable("MonaiApplicationEntities"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Rest.InferenceRequest", b => + { + b.Property("InferenceRequestId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("InputMetadata") + .HasColumnType("TEXT"); + + b.Property("InputResources") + .HasColumnType("TEXT"); + + b.Property("OutputResources") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("TransactionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TryCount") + .HasColumnType("INTEGER"); + + b.HasKey("InferenceRequestId"); + + b.HasIndex(new[] { "InferenceRequestId" }, "idx_inferencerequest_inferencerequestid") + .IsUnique(); + + b.HasIndex(new[] { "State" }, "idx_inferencerequest_state"); + + b.HasIndex(new[] { "TransactionId" }, "idx_inferencerequest_transactionid") + .IsUnique(); + + b.ToTable("InferenceRequests"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.SourceApplicationEntity", b => + { + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("AeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DateTimeUpdated") + .HasColumnType("TEXT"); + + b.Property("HostIp") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.HasIndex(new[] { "Name", "AeTitle", "HostIp" }, "idx_source_all") + .IsUnique() + .HasDatabaseName("idx_source_all2"); + + b.HasIndex(new[] { "Name" }, "idx_source_name") + .IsUnique(); + + b.ToTable("SourceApplicationEntities"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Storage.Payload", b => + { + b.Property("PayloadId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DataOrigins") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DataTrigger") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DestinationFolder") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Files") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MachineName") + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.Property("TaskId") + .HasColumnType("TEXT"); + + b.Property("Timeout") + .HasColumnType("INTEGER"); + + b.Property("WorkflowInstanceId") + .HasColumnType("TEXT"); + + b.HasKey("PayloadId"); + + b.HasIndex(new[] { "CorrelationId", "PayloadId" }, "idx_payload_ids") + .IsUnique(); + + b.HasIndex(new[] { "State" }, "idx_payload_state"); + + b.ToTable("Payloads"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.VirtualApplicationEntity", b => + { + b.Property("Name") + .HasColumnType("TEXT") + .HasColumnOrder(0); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DateTimeUpdated") + .HasColumnType("TEXT"); + + b.Property("PlugInAssemblies") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("VirtualAeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Workflows") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.HasIndex(new[] { "Name" }, "idx_virtualae_name") + .IsUnique(); + + b.ToTable("VirtualApplicationEntities"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Database.Api.StorageMetadataWrapper", b => + { + b.Property("CorrelationId") + .HasColumnType("TEXT"); + + b.Property("Identity") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("IsUploaded") + .HasColumnType("INTEGER"); + + b.Property("TypeName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("CorrelationId", "Identity"); + + b.HasIndex(new[] { "CorrelationId" }, "idx_storagemetadata_correlation"); + + b.HasIndex(new[] { "CorrelationId", "Identity" }, "idx_storagemetadata_ids"); + + b.HasIndex(new[] { "IsUploaded" }, "idx_storagemetadata_uploaded"); + + b.ToTable("StorageMetadataWrapperEntities"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Database/EntityFramework/Migrations/20231207154732_Hl7Plugins.cs b/src/Database/EntityFramework/Migrations/20231207154732_Hl7Plugins.cs new file mode 100755 index 000000000..c86631267 --- /dev/null +++ b/src/Database/EntityFramework/Migrations/20231207154732_Hl7Plugins.cs @@ -0,0 +1,27 @@ + +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Monai.Deploy.InformaticsGateway.Database.Migrations +{ + public partial class Hl7Plugins : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastModified", + table: "Hl7ApplicationConfig", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LastModified", + table: "Hl7ApplicationConfig"); + } + } +} diff --git a/src/Database/EntityFramework/Migrations/InformaticsGatewayContextModelSnapshot.cs b/src/Database/EntityFramework/Migrations/InformaticsGatewayContextModelSnapshot.cs old mode 100644 new mode 100755 index f85fc6347..8eb10b0df --- a/src/Database/EntityFramework/Migrations/InformaticsGatewayContextModelSnapshot.cs +++ b/src/Database/EntityFramework/Migrations/InformaticsGatewayContextModelSnapshot.cs @@ -15,9 +15,45 @@ partial class InformaticsGatewayContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "6.0.21"); + modelBuilder.HasAnnotation("ProductVersion", "6.0.25"); - modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.DestinationApplicationEntity", b => + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Hl7ApplicationConfigEntity", b => + { + b.Property("Name") + .HasColumnType("TEXT") + .HasColumnOrder(0); + + b.Property("DataLink") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DataMapping") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("PlugInAssemblies") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SendingId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.HasIndex(new[] { "Name" }, "idx_hl7_name") + .IsUnique(); + + b.ToTable("Hl7ApplicationConfig"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Models.DestinationApplicationEntity", b => { b.Property("Name") .HasColumnType("TEXT"); @@ -56,7 +92,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("DestinationApplicationEntities"); }); - modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.DicomAssociationInfo", b => + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Models.DicomAssociationInfo", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -106,7 +142,93 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("DicomAssociationHistories"); }); - modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.MonaiApplicationEntity", b => + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Models.ExternalAppDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DestinationFolder") + .HasColumnType("TEXT"); + + b.Property("ExportTaskID") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PatientId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PatientIdOutBound") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StudyInstanceUid") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StudyInstanceUidOutBound") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ExternalAppDetails"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Models.HL7DestinationEntity", b => + { + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("AeTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DateTimeCreated") + .HasColumnType("TEXT"); + + b.Property("DateTimeUpdated") + .HasColumnType("TEXT"); + + b.Property("HostIp") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.HasIndex(new[] { "Name" }, "idx_destination_name") + .IsUnique() + .HasDatabaseName("idx_destination_name1"); + + b.HasIndex(new[] { "Name", "AeTitle", "HostIp", "Port" }, "idx_source_all") + .IsUnique() + .HasDatabaseName("idx_source_all1"); + + b.ToTable("HL7DestinationEntities"); + }); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Models.MonaiApplicationEntity", b => { b.Property("Name") .HasColumnType("TEXT") @@ -239,7 +361,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex(new[] { "Name", "AeTitle", "HostIp" }, "idx_source_all") .IsUnique() - .HasDatabaseName("idx_source_all1"); + .HasDatabaseName("idx_source_all2"); b.HasIndex(new[] { "Name" }, "idx_source_name") .IsUnique(); @@ -268,6 +390,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DateTimeCreated") .HasColumnType("TEXT"); + b.Property("DestinationFolder") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("Files") .IsRequired() .HasColumnType("TEXT"); diff --git a/src/Database/EntityFramework/Monai.Deploy.InformaticsGateway.Database.EntityFramework.csproj b/src/Database/EntityFramework/Monai.Deploy.InformaticsGateway.Database.EntityFramework.csproj old mode 100644 new mode 100755 index b99824dd2..d9ad95499 --- a/src/Database/EntityFramework/Monai.Deploy.InformaticsGateway.Database.EntityFramework.csproj +++ b/src/Database/EntityFramework/Monai.Deploy.InformaticsGateway.Database.EntityFramework.csproj @@ -42,12 +42,12 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/src/Database/EntityFramework/Repositories/DestinationApplicationEntityRepository.cs b/src/Database/EntityFramework/Repositories/DestinationApplicationEntityRepository.cs index 459631ac3..4a3f1cdc2 100755 --- a/src/Database/EntityFramework/Repositories/DestinationApplicationEntityRepository.cs +++ b/src/Database/EntityFramework/Repositories/DestinationApplicationEntityRepository.cs @@ -20,7 +20,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.Api.Logging; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; diff --git a/src/Database/EntityFramework/Repositories/DicomAssociationInfoRepository.cs b/src/Database/EntityFramework/Repositories/DicomAssociationInfoRepository.cs index b144ac2c5..2e000e947 100755 --- a/src/Database/EntityFramework/Repositories/DicomAssociationInfoRepository.cs +++ b/src/Database/EntityFramework/Repositories/DicomAssociationInfoRepository.cs @@ -19,7 +19,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.Api.Logging; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; diff --git a/src/Database/EntityFramework/Repositories/ExternalAppDetailsRepository.cs b/src/Database/EntityFramework/Repositories/ExternalAppDetailsRepository.cs new file mode 100755 index 000000000..52fce4a50 --- /dev/null +++ b/src/Database/EntityFramework/Repositories/ExternalAppDetailsRepository.cs @@ -0,0 +1,110 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 Ardalis.GuardClauses; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api.Models; +using Monai.Deploy.InformaticsGateway.Configuration; +using Polly.Retry; +using Microsoft.Extensions.Logging; +using Polly; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Monai.Deploy.InformaticsGateway.Database.Api.Logging; + +namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Repositories +{ + public class ExternalAppDetailsRepository : IExternalAppDetailsRepository, IDisposable + { + private readonly ILogger _logger; + private readonly IServiceScope _scope; + private readonly InformaticsGatewayContext _informaticsGatewayContext; + private readonly AsyncRetryPolicy _retryPolicy; + private readonly DbSet _dataset; + private bool _disposedValue; + + public ExternalAppDetailsRepository( + IServiceScopeFactory serviceScopeFactory, + ILogger logger, + IOptions options) + { + Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); + Guard.Against.Null(options, nameof(options)); + + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _scope = serviceScopeFactory.CreateScope(); + _informaticsGatewayContext = _scope.ServiceProvider.GetRequiredService(); + _retryPolicy = Policy.Handle().WaitAndRetryAsync( + options.Value.Retries.RetryDelays, + (exception, timespan, count, context) => _logger.DatabaseErrorRetry(timespan, count, exception)); + _dataset = _informaticsGatewayContext.Set(); + } + + public async Task AddAsync(ExternalAppDetails details, CancellationToken cancellationToken = default) + { + Guard.Against.Null(details, nameof(details)); + + await _retryPolicy.ExecuteAsync(async () => + { + var result = await _dataset.AddAsync(details, cancellationToken).ConfigureAwait(false); + await _informaticsGatewayContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + }).ConfigureAwait(false); + } + + public async Task> GetAsync(string studyInstanceId, CancellationToken cancellationToken = default) + { + return await _dataset + .Where(t => t.StudyInstanceUid == studyInstanceId).ToListAsync(cancellationToken) + .ConfigureAwait(false); + } + + public async Task GetByPatientIdOutboundAsync(string patientId, CancellationToken cancellationToken) + { + return await _dataset + .FirstOrDefaultAsync(t => t.PatientIdOutBound == patientId, cancellationToken) + .ConfigureAwait(false); + } + + public async Task GetByStudyIdOutboundAsync(string studyInstanceId, CancellationToken cancellationToken) + { + return await _dataset + .FirstOrDefaultAsync(t => t.StudyInstanceUidOutBound == studyInstanceId, cancellationToken) + .ConfigureAwait(false); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _informaticsGatewayContext.Dispose(); + _scope.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Database/EntityFramework/Repositories/HL7DestinationEntityRepository.cs b/src/Database/EntityFramework/Repositories/HL7DestinationEntityRepository.cs new file mode 100644 index 000000000..2c0ab03ae --- /dev/null +++ b/src/Database/EntityFramework/Repositories/HL7DestinationEntityRepository.cs @@ -0,0 +1,143 @@ +/* + * Copyright 2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.Linq.Expressions; +using Ardalis.GuardClauses; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api.Models; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api.Logging; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Polly; +using Polly.Retry; + +namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Repositories +{ + public class HL7DestinationEntityRepository : IHL7DestinationEntityRepository, IDisposable + { + private readonly ILogger _logger; + private readonly IServiceScope _scope; + private readonly InformaticsGatewayContext _informaticsGatewayContext; + private readonly AsyncRetryPolicy _retryPolicy; + private readonly DbSet _dataset; + private bool _disposedValue; + + public HL7DestinationEntityRepository( + IServiceScopeFactory serviceScopeFactory, + ILogger logger, + IOptions options) + { + Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); + Guard.Against.Null(options, nameof(options)); + + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _scope = serviceScopeFactory.CreateScope(); + _informaticsGatewayContext = _scope.ServiceProvider.GetRequiredService(); + _retryPolicy = Policy.Handle().WaitAndRetryAsync( + options.Value.Retries.RetryDelays, + (exception, timespan, count, context) => _logger.DatabaseErrorRetry(timespan, count, exception)); + _dataset = _informaticsGatewayContext.Set(); + } + + public async Task AddAsync(HL7DestinationEntity item, CancellationToken cancellationToken = default) + { + Guard.Against.Null(item, nameof(item)); + + return await _retryPolicy.ExecuteAsync(async () => + { + var result = await _dataset.AddAsync(item, cancellationToken).ConfigureAwait(false); + await _informaticsGatewayContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + return result.Entity; + }).ConfigureAwait(false); + } + + public async Task ContainsAsync(Expression> predicate, CancellationToken cancellationToken = default) + { + return await _retryPolicy.ExecuteAsync(async () => + { + var func = predicate.Compile(); + return await Task.FromResult(_dataset.Any(func)).ConfigureAwait(false); + }).ConfigureAwait(false); + } + + public async Task FindByNameAsync(string name, CancellationToken cancellationToken = default) + { + Guard.Against.NullOrWhiteSpace(name, nameof(name)); + + return await _retryPolicy.ExecuteAsync(async () => + { + return await _dataset.FirstOrDefaultAsync(p => p.Name.Equals(name), cancellationToken).ConfigureAwait(false); + }).ConfigureAwait(false); + } + + public async Task RemoveAsync(HL7DestinationEntity entity, CancellationToken cancellationToken = default) + { + Guard.Against.Null(entity, nameof(entity)); + + return await _retryPolicy.ExecuteAsync(async () => + { + var result = _dataset.Remove(entity); + await _informaticsGatewayContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + return result.Entity; + }).ConfigureAwait(false); + } + + public async Task> ToListAsync(CancellationToken cancellationToken = default) + { + return await _retryPolicy.ExecuteAsync(async () => + { + return await _dataset.ToListAsync(cancellationToken).ConfigureAwait(false); + }).ConfigureAwait(false); + } + + public async Task UpdateAsync(HL7DestinationEntity entity, CancellationToken cancellationToken = default) + { + Guard.Against.Null(entity, nameof(entity)); + + return await _retryPolicy.ExecuteAsync(async () => + { + var result = _dataset.Update(entity); + await _informaticsGatewayContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + return result.Entity; + }).ConfigureAwait(false); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _informaticsGatewayContext.Dispose(); + _scope.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Database/EntityFramework/Repositories/Hl7ApplicationConfigRepository.cs b/src/Database/EntityFramework/Repositories/Hl7ApplicationConfigRepository.cs new file mode 100755 index 000000000..898181503 --- /dev/null +++ b/src/Database/EntityFramework/Repositories/Hl7ApplicationConfigRepository.cs @@ -0,0 +1,95 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 Ardalis.GuardClauses; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api; +using Monai.Deploy.InformaticsGateway.Database.Api.Logging; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Polly; +using Polly.Retry; + +namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Repositories +{ + public class Hl7ApplicationConfigRepository : IHl7ApplicationConfigRepository + { + private readonly ILogger _logger; + private readonly InformaticsGatewayContext _informaticsGatewayContext; + private readonly AsyncRetryPolicy _retryPolicy; + private readonly DbSet _dataset; + + public Hl7ApplicationConfigRepository(ILogger logger, + IOptions options, IServiceScopeFactory serviceScopeFactory) + { + Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); + Guard.Against.Null(options, nameof(options)); + + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + var scope = serviceScopeFactory.CreateScope(); + + _informaticsGatewayContext = scope.ServiceProvider.GetRequiredService(); + _retryPolicy = Policy.Handle().WaitAndRetryAsync( + options.Value.Retries.RetryDelays, + (exception, timespan, count, context) => _logger.DatabaseErrorRetry(timespan, count, exception)); + _dataset = _informaticsGatewayContext.Set(); + } + + public Task> GetAllAsync(CancellationToken cancellationToken = default) => + _retryPolicy.ExecuteAsync(() => _dataset.ToListAsync(cancellationToken)); + + public Task GetByIdAsync(string id) => + _retryPolicy.ExecuteAsync(() => _dataset.FirstOrDefaultAsync(x => x.Id.Equals(id))); + + public Task DeleteAsync(string id, CancellationToken cancellationToken) + { + return _retryPolicy.ExecuteAsync(async () => + { + var entity = await GetByIdAsync(id).ConfigureAwait(false) ?? throw new DatabaseException("Failed to delete entity."); + var result = _dataset.Remove(entity); + await _informaticsGatewayContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + return result.Entity; + }); + } + + public Task CreateAsync(Hl7ApplicationConfigEntity configEntity, + CancellationToken cancellationToken = default) + { + return _retryPolicy.ExecuteAsync(async () => + { + var result = await _dataset.AddAsync(configEntity, cancellationToken).ConfigureAwait(false); + await _informaticsGatewayContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + return result.Entity; + }); + } + + public Task UpdateAsync(Hl7ApplicationConfigEntity configEntity, + CancellationToken cancellationToken = default) + { + return _retryPolicy.ExecuteAsync(async () => + { + var result = _dataset.Update(configEntity); + await _informaticsGatewayContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + return result.Entity; + })!; + } + } +} diff --git a/src/Database/EntityFramework/Repositories/MonaiApplicationEntityRepository.cs b/src/Database/EntityFramework/Repositories/MonaiApplicationEntityRepository.cs index f97696edf..e8001a728 100755 --- a/src/Database/EntityFramework/Repositories/MonaiApplicationEntityRepository.cs +++ b/src/Database/EntityFramework/Repositories/MonaiApplicationEntityRepository.cs @@ -20,7 +20,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.Api.Logging; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; diff --git a/src/Database/EntityFramework/Test/DestinationApplicationEntityRepositoryTest.cs b/src/Database/EntityFramework/Test/DestinationApplicationEntityRepositoryTest.cs index 60170d4a2..0d5d923d6 100755 --- a/src/Database/EntityFramework/Test/DestinationApplicationEntityRepositoryTest.cs +++ b/src/Database/EntityFramework/Test/DestinationApplicationEntityRepositoryTest.cs @@ -19,9 +19,9 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Monai.Deploy.InformaticsGateway.Configuration; -using Monai.Deploy.InformaticsGateway.Api; using Monai.Deploy.InformaticsGateway.Database.EntityFramework.Repositories; using Moq; +using Monai.Deploy.InformaticsGateway.Api.Models; namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Test { diff --git a/src/Database/EntityFramework/Test/DicomAssociationInfoRepositoryTest.cs b/src/Database/EntityFramework/Test/DicomAssociationInfoRepositoryTest.cs index 8a3d6eb7d..d0acc3330 100755 --- a/src/Database/EntityFramework/Test/DicomAssociationInfoRepositoryTest.cs +++ b/src/Database/EntityFramework/Test/DicomAssociationInfoRepositoryTest.cs @@ -18,7 +18,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.EntityFramework.Repositories; using Moq; diff --git a/src/Database/EntityFramework/Test/ExternalAppDetailsRepositoryTest.cs b/src/Database/EntityFramework/Test/ExternalAppDetailsRepositoryTest.cs new file mode 100755 index 000000000..77d991e3c --- /dev/null +++ b/src/Database/EntityFramework/Test/ExternalAppDetailsRepositoryTest.cs @@ -0,0 +1,116 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api.Models; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.EntityFramework.Repositories; +using Moq; + + +namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Test +{ + [Collection("SqliteDatabase")] + public class ExternalAppDetailsRepositoryTest + { + private readonly SqliteDatabaseFixture _databaseFixture; + + private readonly Mock _serviceScopeFactory; + private readonly Mock> _logger; + private readonly IOptions _options; + + private readonly Mock _serviceScope; + private readonly IServiceProvider _serviceProvider; + + public ExternalAppDetailsRepositoryTest(SqliteDatabaseFixture databaseFixture) + { + _databaseFixture = databaseFixture ?? throw new ArgumentNullException(nameof(databaseFixture)); + _databaseFixture.InitDatabaseWithExternalAppDetailsEntries(); + + _serviceScopeFactory = new Mock(); + _logger = new Mock>(); + _options = Options.Create(new DatabaseOptions()); + + _serviceScope = new Mock(); + var services = new ServiceCollection(); + services.AddScoped(p => _logger.Object); + services.AddScoped(p => databaseFixture.DatabaseContext); + + _serviceProvider = services.BuildServiceProvider(); + _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); + _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); + + _options.Value.Retries.DelaysMilliseconds = new[] { 1, 1, 1 }; + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + } + [Fact] + public async Task GivenDestinationExternalAppInTheDatabase_WhenGetAsyncCalled_ExpectEntitieToBeReturned() + { + var store = new ExternalAppDetailsRepository(_serviceScopeFactory.Object, _logger.Object, _options); + var startTime = DateTime.Now; + var endTime = DateTime.MinValue; + + var expected = _databaseFixture.DatabaseContext.Set() + .Where(t => t.StudyInstanceUid == "1"); + var actual = await store.GetAsync("1").ConfigureAwait(false); + + Assert.NotNull(actual); + Assert.Equal(expected, actual); + } + + [Fact] + public async Task GivenDestinationExternalAppInTheDatabase_WhenGetAsyncCalled_ExpectEntitieToBeReturned2() + { + var store = new ExternalAppDetailsRepository(_serviceScopeFactory.Object, _logger.Object, _options); + var startTime = DateTime.Now; + var endTime = DateTime.MinValue; + + var expected = _databaseFixture.DatabaseContext.Set() + .Where(t => t.PatientIdOutBound == "2") + .Take(1).First(); + var actual = await store.GetByPatientIdOutboundAsync("2", new CancellationToken()).ConfigureAwait(false); + + Assert.NotNull(actual); + Assert.Equal(expected, actual); + } + + [Fact] + public async Task GivenDestinationExternalAppInTheDatabase_WhenAddingToDatabase_ExpectItToBeSaved() + { + var association = new ExternalAppDetails + { + StudyInstanceUid = "3", + WorkflowInstanceId = "calling", + CorrelationId = Guid.NewGuid().ToString(), + DateTimeCreated = DateTime.UtcNow, + ExportTaskID = "host" + }; + + var store = new ExternalAppDetailsRepository(_serviceScopeFactory.Object, _logger.Object, _options); + await store.AddAsync(association).ConfigureAwait(false); + var actual = await _databaseFixture.DatabaseContext.Set().FirstOrDefaultAsync(p => p.StudyInstanceUid.Equals(association.StudyInstanceUid)).ConfigureAwait(false); + + Assert.NotNull(actual); + Assert.Equal(association.DateTimeCreated, actual!.DateTimeCreated); + Assert.Equal(association.WorkflowInstanceId, actual!.WorkflowInstanceId); + Assert.Equal(association.ExportTaskID, actual!.ExportTaskID); + Assert.Equal(association.CorrelationId, actual!.CorrelationId); + } + } +} diff --git a/src/Database/EntityFramework/Test/HL7DestinationEntityRepositoryTest.cs b/src/Database/EntityFramework/Test/HL7DestinationEntityRepositoryTest.cs new file mode 100644 index 000000000..1f24a40f8 --- /dev/null +++ b/src/Database/EntityFramework/Test/HL7DestinationEntityRepositoryTest.cs @@ -0,0 +1,159 @@ +/* + * Copyright 2022-2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Api.Models; +using Monai.Deploy.InformaticsGateway.Database.EntityFramework.Repositories; +using Moq; + +namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Test +{ + [Collection("SqliteDatabase")] + public class HL7DestinationEntityRepositoryTest + { + private readonly SqliteDatabaseFixture _databaseFixture; + + private readonly Mock _serviceScopeFactory; + private readonly Mock> _logger; + private readonly IOptions _options; + + private readonly Mock _serviceScope; + private readonly IServiceProvider _serviceProvider; + + public HL7DestinationEntityRepositoryTest(SqliteDatabaseFixture databaseFixture) + { + _databaseFixture = databaseFixture ?? throw new ArgumentNullException(nameof(databaseFixture)); + _databaseFixture.InitDatabaseWithHL7DestinationEntities(); + + _serviceScopeFactory = new Mock(); + _logger = new Mock>(); + _options = Options.Create(new DatabaseOptions()); + + _serviceScope = new Mock(); + var services = new ServiceCollection(); + services.AddScoped(p => _logger.Object); + services.AddScoped(p => databaseFixture.DatabaseContext); + + _serviceProvider = services.BuildServiceProvider(); + _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); + _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); + + _options.Value.Retries.DelaysMilliseconds = new[] { 1, 1, 1 }; + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + } + + [Fact] + public async Task GivenAHL7DestinationEntity_WhenAddingToDatabase_ExpectItToBeSaved() + { + var aet = new HL7DestinationEntity { AeTitle = "AET", HostIp = "1.2.3.4", Port = 114, Name = "AET" }; + + var store = new HL7DestinationEntityRepository(_serviceScopeFactory.Object, _logger.Object, _options); + await store.AddAsync(aet).ConfigureAwait(false); + var actual = await _databaseFixture.DatabaseContext.Set().FirstOrDefaultAsync(p => p.Name.Equals(aet.Name)).ConfigureAwait(false); + + Assert.NotNull(actual); + Assert.Equal(aet.AeTitle, actual!.AeTitle); + Assert.Equal(aet.HostIp, actual!.HostIp); + Assert.Equal(aet.Port, actual!.Port); + Assert.Equal(aet.Name, actual!.Name); + } + + [Fact] + public async Task GivenAExpressionFilter_WhenContainsAsyncIsCalled_ExpectItToReturnMatchingObjects() + { + var store = new HL7DestinationEntityRepository(_serviceScopeFactory.Object, _logger.Object, _options); + + var result = await store.ContainsAsync(p => p.AeTitle == "AET1").ConfigureAwait(false); + Assert.True(result); + result = await store.ContainsAsync(p => p.AeTitle.Equals("AET1", StringComparison.Ordinal)).ConfigureAwait(false); + Assert.True(result); + result = await store.ContainsAsync(p => p.AeTitle != "AET1" && p.Port == 114 && p.HostIp == "1.2.3.4").ConfigureAwait(false); + Assert.True(result); + result = await store.ContainsAsync(p => p.Port == 114 && p.HostIp == "1.2.3.4").ConfigureAwait(false); + Assert.True(result); + result = await store.ContainsAsync(p => p.Port == 999).ConfigureAwait(false); + Assert.False(result); + } + + [Fact] + public async Task GivenAAETitleName_WhenFindByNameAsyncIsCalled_ExpectItToReturnMatchingEntity() + { + var store = new HL7DestinationEntityRepository(_serviceScopeFactory.Object, _logger.Object, _options); + + var actual = await store.FindByNameAsync("AET1").ConfigureAwait(false); + Assert.NotNull(actual); + Assert.Equal("AET1", actual!.AeTitle); + Assert.Equal("1.2.3.4", actual!.HostIp); + Assert.Equal(114, actual!.Port); + Assert.Equal("AET1", actual!.Name); + + actual = await store.FindByNameAsync("AET6").ConfigureAwait(false); + Assert.Null(actual); + } + + [Fact] + public async Task GivenAHL7DestinationEntity_WhenRemoveIsCalled_ExpectItToDeleted() + { + var store = new HL7DestinationEntityRepository(_serviceScopeFactory.Object, _logger.Object, _options); + + var expected = await store.FindByNameAsync("AET5").ConfigureAwait(false); + Assert.NotNull(expected); + + var actual = await store.RemoveAsync(expected!).ConfigureAwait(false); + Assert.Same(expected, actual); + + var dbResult = await _databaseFixture.DatabaseContext.Set().FirstOrDefaultAsync(p => p.Name == "AET5").ConfigureAwait(false); + Assert.Null(dbResult); + } + + [Fact] + public async Task GivenHL7DestinationEntitiesInTheDatabase_WhenToListIsCalled_ExpectAllEntitiesToBeReturned() + { + var store = new HL7DestinationEntityRepository(_serviceScopeFactory.Object, _logger.Object, _options); + + var expected = await _databaseFixture.DatabaseContext.Set().ToListAsync().ConfigureAwait(false); + var actual = await store.ToListAsync().ConfigureAwait(false); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task GivenAHL7DestinationEntity_WhenUpdatedIsCalled_ExpectItToSaved() + { + var store = new HL7DestinationEntityRepository(_serviceScopeFactory.Object, _logger.Object, _options); + + var expected = await store.FindByNameAsync("AET3").ConfigureAwait(false); + Assert.NotNull(expected); + + expected!.AeTitle = "AET100"; + expected!.Port = 1000; + expected!.HostIp = "loalhost"; + + var actual = await store.UpdateAsync(expected).ConfigureAwait(false); + Assert.Equal(expected, actual); + + var dbResult = await store.FindByNameAsync("AET3").ConfigureAwait(false); + Assert.NotNull(dbResult); + Assert.Equal(expected.AeTitle, dbResult!.AeTitle); + Assert.Equal(expected.HostIp, dbResult!.HostIp); + Assert.Equal(expected.Port, dbResult!.Port); + } + } +} diff --git a/src/Database/EntityFramework/Test/InMemoryDatabaseFixture.cs b/src/Database/EntityFramework/Test/InMemoryDatabaseFixture.cs old mode 100644 new mode 100755 index 2aea0684a..01f8d8457 --- a/src/Database/EntityFramework/Test/InMemoryDatabaseFixture.cs +++ b/src/Database/EntityFramework/Test/InMemoryDatabaseFixture.cs @@ -16,6 +16,7 @@ using Microsoft.EntityFrameworkCore; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Test { diff --git a/src/Database/EntityFramework/Test/Monai.Deploy.InformaticsGateway.Database.EntityFramework.Test.csproj b/src/Database/EntityFramework/Test/Monai.Deploy.InformaticsGateway.Database.EntityFramework.Test.csproj old mode 100644 new mode 100755 index c9ad64d69..abff60683 --- a/src/Database/EntityFramework/Test/Monai.Deploy.InformaticsGateway.Database.EntityFramework.Test.csproj +++ b/src/Database/EntityFramework/Test/Monai.Deploy.InformaticsGateway.Database.EntityFramework.Test.csproj @@ -25,7 +25,7 @@ - + diff --git a/src/Database/EntityFramework/Test/MonaiApplicationEntityRepositoryTest.cs b/src/Database/EntityFramework/Test/MonaiApplicationEntityRepositoryTest.cs index 609cfecdf..2c6a7558a 100755 --- a/src/Database/EntityFramework/Test/MonaiApplicationEntityRepositoryTest.cs +++ b/src/Database/EntityFramework/Test/MonaiApplicationEntityRepositoryTest.cs @@ -18,7 +18,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.EntityFramework.Repositories; using Moq; diff --git a/src/Database/EntityFramework/Test/SqliteDatabaseFixture.cs b/src/Database/EntityFramework/Test/SqliteDatabaseFixture.cs old mode 100644 new mode 100755 index 7dd549b73..19219a3a3 --- a/src/Database/EntityFramework/Test/SqliteDatabaseFixture.cs +++ b/src/Database/EntityFramework/Test/SqliteDatabaseFixture.cs @@ -16,6 +16,7 @@ using Microsoft.EntityFrameworkCore; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Test { @@ -59,6 +60,25 @@ public void InitDatabaseWithDestinationApplicationEntities() DatabaseContext.SaveChanges(); } + public void InitDatabaseWithHL7DestinationEntities() + { + var aet1 = new HL7DestinationEntity { AeTitle = "AET1", HostIp = "1.2.3.4", Port = 114, Name = "AET1" }; + var aet2 = new HL7DestinationEntity { AeTitle = "AET2", HostIp = "1.2.3.4", Port = 114, Name = "AET2" }; + var aet3 = new HL7DestinationEntity { AeTitle = "AET3", HostIp = "1.2.3.4", Port = 114, Name = "AET3" }; + var aet4 = new HL7DestinationEntity { AeTitle = "AET4", HostIp = "1.2.3.4", Port = 114, Name = "AET4" }; + var aet5 = new HL7DestinationEntity { AeTitle = "AET5", HostIp = "1.2.3.4", Port = 114, Name = "AET5" }; + + var set = DatabaseContext.Set(); + set.RemoveRange(set.ToList()); + set.Add(aet1); + set.Add(aet2); + set.Add(aet3); + set.Add(aet4); + set.Add(aet5); + + DatabaseContext.SaveChanges(); + } + public void InitDatabaseWithMonaiApplicationEntities() { var aet1 = new MonaiApplicationEntity { AeTitle = "AET1", Name = "AET1" }; @@ -134,6 +154,17 @@ internal void InitDatabaseWithDicomAssociationInfoEntries() DatabaseContext.SaveChanges(); } + internal void InitDatabaseWithExternalAppDetailsEntries() + { + var ea1 = new ExternalAppDetails { StudyInstanceUid = "1", PatientId = "11", PatientIdOutBound = "1" }; + var ea2 = new ExternalAppDetails { StudyInstanceUid = "2", PatientId = "22", PatientIdOutBound = "2" }; + var set = DatabaseContext.Set(); + set.RemoveRange(set.ToList()); + set.Add(ea1); + set.Add(ea2); + + DatabaseContext.SaveChanges(); + } public void Clear() where T : class { diff --git a/src/Database/EntityFramework/Test/packages.lock.json b/src/Database/EntityFramework/Test/packages.lock.json index e5a296772..95ac88e42 100755 --- a/src/Database/EntityFramework/Test/packages.lock.json +++ b/src/Database/EntityFramework/Test/packages.lock.json @@ -10,11 +10,11 @@ }, "Microsoft.EntityFrameworkCore.InMemory": { "type": "Direct", - "requested": "[6.0.22, )", - "resolved": "6.0.22", - "contentHash": "CcL5ajX+/OkafcP5OMplCBnIgSfaQy5BUjEZQKZ9BlspnwFFucy+wcE0LL1ycOlWcDYGI42FnQ45dD1Kcz+ZKA==", + "requested": "[6.0.25, )", + "resolved": "6.0.25", + "contentHash": "T1wFaHL0WS51PlrSzWfBX2qppMbuIserPUaSwrw6Uhvg4WllsQPKYqFGAZC9bbUAihjgY5es7MIgSEtXYNdLiw==", "dependencies": { - "Microsoft.EntityFrameworkCore": "6.0.22" + "Microsoft.EntityFrameworkCore": "6.0.25" } }, "Microsoft.NET.Test.Sdk": { @@ -102,6 +102,11 @@ "System.Threading.Channels": "6.0.0" } }, + "HL7-dotnetcore": { + "type": "Transitive", + "resolved": "2.36.0", + "contentHash": "N1HLMeIqYuY+4O69ItgZJoDBnnpNkK5N2pClceTJ2nFJxsP48iCsA4iz3tm43Yszi4r/vaThoc3UoLBfGP3vKw==" + }, "Macross.Json.Extensions": { "type": "Transitive", "resolved": "3.0.0", @@ -124,19 +129,19 @@ }, "Microsoft.Data.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "gtIGHbGnRq/h4mFSJYr9BdMObvJV/a67nBubs50VjPDusQARtWJzeVTirDWsbL1qTvGzbbZCD7VE7+s2ixZfow==", + "resolved": "6.0.25", + "contentHash": "rbXNoMg/ylGyJxLcyetojuXFzvDG85M31DfFbqL8veN4P8oG6wmnPwWNn3/bDIEDVvdw15R092dxpobQeQcjGg==", "dependencies": { "SQLitePCLRaw.core": "2.1.2" } }, "Microsoft.EntityFrameworkCore": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "vNe+y8ZsEf1CsfmfYttfKAz/IgCCtphgguvao0HWNJNdjZf9cabD288nZJ17b/WaQMWXhLwYAsofk8vNVkfTOA==", + "resolved": "6.0.25", + "contentHash": "txcqw2xrmvMoTIgzAdUk8JHLELofGgTK3i6glswVZs4SC8BOU1M/iSAtwMIVtAtfzxuBIUAbHPx+Ly6lfkYe7g==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "6.0.22", - "Microsoft.EntityFrameworkCore.Analyzers": "6.0.22", + "Microsoft.EntityFrameworkCore.Abstractions": "6.0.25", + "Microsoft.EntityFrameworkCore.Analyzers": "6.0.25", "Microsoft.Extensions.Caching.Memory": "6.0.1", "Microsoft.Extensions.DependencyInjection": "6.0.1", "Microsoft.Extensions.Logging": "6.0.0", @@ -146,39 +151,39 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "82SZfdrLe7bdDB8/3INV0UULvlUzsdHkrEYylDCrzFXRWHXG9eO5jJQjRHU8j9XkGIN+MSPgIlczBnqeDvB36A==" + "resolved": "6.0.25", + "contentHash": "i6UpdWqWxSBbIFOkaMoubM40yIjTZO+0rIUkY5JRltSeFI4PzncBBQcNVNXXjAmiLXF/xY0xTS+ykClbkV46Yg==" }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "W7yfdEbEuS1OPPxU0EOA6haqI4uvzs7OwHKh81DiJFn3NFNP2ztSovkOzBDhTwHX0j+OySsAj3BEJhuzTVYIVw==", + "resolved": "6.0.25", + "contentHash": "ci2lR++x7R7LR71+HoeRnB9Z5VeOQ1ILLbFRhsjjWZyLrAMkdq7TK9Ll47jo1TXDWF8Ddeap1JgcptgPKkWSRA==", "dependencies": { - "Microsoft.EntityFrameworkCore": "6.0.22", + "Microsoft.EntityFrameworkCore": "6.0.25", "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" } }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "EDKnYZtxq7P131xxLsEokda86WnFRiVAveLVAYR8kzyWl/UwTpf/RS2m2FrbH/U8vX3A+IQNpabtxcjtCUrY0g==", + "resolved": "6.0.25", + "contentHash": "vaQNuXgUN0nIzFXQiPSb9iAaJqLvZA164Sx9mjF5rFQS5cwQ/AiymF0e4J0QH3P07Mf3zEVZE5u2fTO0NacuMQ==", "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.22", + "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.25", "SQLitePCLRaw.bundle_e_sqlite3": "2.1.2" } }, "Microsoft.EntityFrameworkCore.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "xSU77ORQgwlD+s5Cmlk9DzoSCu5oxlHLuQl+v5zAZ0Uv5yH17hp02TBfz3x9nBA+CrIsqaLjGEuyZmLDf/5ATw==", + "resolved": "6.0.25", + "contentHash": "IU4E8I9FS2sUVxJJ0w/4jogLQ8C0zvu/SO6b1tRmiiCtTrHhjUB0tqhxjrFnDXZ/mpCJOElw50+qhbcElm0CYw==", "dependencies": { - "Microsoft.Data.Sqlite.Core": "6.0.22", - "Microsoft.EntityFrameworkCore.Relational": "6.0.22", + "Microsoft.Data.Sqlite.Core": "6.0.25", + "Microsoft.EntityFrameworkCore.Relational": "6.0.25", "Microsoft.Extensions.DependencyModel": "6.0.0" } }, @@ -392,8 +397,8 @@ }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -403,10 +408,10 @@ }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } @@ -1468,11 +1473,12 @@ "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { + "HL7-dotnetcore": "[2.36.0, )", "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.22, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.25, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[1.0.4, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging": "[1.0.5, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Storage": "[0.2.18, )", "fo-dicom": "[5.1.1, )" } @@ -1502,8 +1508,8 @@ "monai.deploy.informaticsgateway.database.entityframework": { "type": "Project", "dependencies": { - "Microsoft.EntityFrameworkCore": "[6.0.22, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.22, )", + "Microsoft.EntityFrameworkCore": "[6.0.25, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.25, )", "Microsoft.Extensions.Configuration.FileExtensions": "[6.0.0, )", "Microsoft.Extensions.Configuration.Json": "[6.0.0, )", "Monai.Deploy.InformaticsGateway.Api": "[0.4.1, )", diff --git a/src/Database/EntityFramework/packages.lock.json b/src/Database/EntityFramework/packages.lock.json index 0d9e1c3e1..91f7b6eac 100755 --- a/src/Database/EntityFramework/packages.lock.json +++ b/src/Database/EntityFramework/packages.lock.json @@ -4,12 +4,12 @@ "net6.0": { "Microsoft.EntityFrameworkCore": { "type": "Direct", - "requested": "[6.0.22, )", - "resolved": "6.0.22", - "contentHash": "vNe+y8ZsEf1CsfmfYttfKAz/IgCCtphgguvao0HWNJNdjZf9cabD288nZJ17b/WaQMWXhLwYAsofk8vNVkfTOA==", + "requested": "[6.0.25, )", + "resolved": "6.0.25", + "contentHash": "txcqw2xrmvMoTIgzAdUk8JHLELofGgTK3i6glswVZs4SC8BOU1M/iSAtwMIVtAtfzxuBIUAbHPx+Ly6lfkYe7g==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "6.0.22", - "Microsoft.EntityFrameworkCore.Analyzers": "6.0.22", + "Microsoft.EntityFrameworkCore.Abstractions": "6.0.25", + "Microsoft.EntityFrameworkCore.Analyzers": "6.0.25", "Microsoft.Extensions.Caching.Memory": "6.0.1", "Microsoft.Extensions.DependencyInjection": "6.0.1", "Microsoft.Extensions.Logging": "6.0.0", @@ -19,21 +19,21 @@ }, "Microsoft.EntityFrameworkCore.Design": { "type": "Direct", - "requested": "[6.0.22, )", - "resolved": "6.0.22", - "contentHash": "es9TKd0cpM263Ou0QMEETN7MDzD7kXDkThiiXl1+c/69v97AZlzeLoM5tDdC0RC4L74ZWyk3+WMnoDPL93DDyQ==", + "requested": "[6.0.25, )", + "resolved": "6.0.25", + "contentHash": "YawyMKj1f+GkwHrxMIf9tX84sMGgLFa5YoRmyuUugGhffiubkVLYIrlm4W0uSy2NzX4t6+V7keFLQf7lRQvDmA==", "dependencies": { "Humanizer.Core": "2.8.26", - "Microsoft.EntityFrameworkCore.Relational": "6.0.22" + "Microsoft.EntityFrameworkCore.Relational": "6.0.25" } }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "Direct", - "requested": "[6.0.22, )", - "resolved": "6.0.22", - "contentHash": "EDKnYZtxq7P131xxLsEokda86WnFRiVAveLVAYR8kzyWl/UwTpf/RS2m2FrbH/U8vX3A+IQNpabtxcjtCUrY0g==", + "requested": "[6.0.25, )", + "resolved": "6.0.25", + "contentHash": "vaQNuXgUN0nIzFXQiPSb9iAaJqLvZA164Sx9mjF5rFQS5cwQ/AiymF0e4J0QH3P07Mf3zEVZE5u2fTO0NacuMQ==", "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.22", + "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.25", "SQLitePCLRaw.bundle_e_sqlite3": "2.1.2" } }, @@ -110,6 +110,11 @@ "System.Threading.Channels": "6.0.0" } }, + "HL7-dotnetcore": { + "type": "Transitive", + "resolved": "2.36.0", + "contentHash": "N1HLMeIqYuY+4O69ItgZJoDBnnpNkK5N2pClceTJ2nFJxsP48iCsA4iz3tm43Yszi4r/vaThoc3UoLBfGP3vKw==" + }, "Humanizer.Core": { "type": "Transitive", "resolved": "2.8.26", @@ -132,38 +137,38 @@ }, "Microsoft.Data.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "gtIGHbGnRq/h4mFSJYr9BdMObvJV/a67nBubs50VjPDusQARtWJzeVTirDWsbL1qTvGzbbZCD7VE7+s2ixZfow==", + "resolved": "6.0.25", + "contentHash": "rbXNoMg/ylGyJxLcyetojuXFzvDG85M31DfFbqL8veN4P8oG6wmnPwWNn3/bDIEDVvdw15R092dxpobQeQcjGg==", "dependencies": { "SQLitePCLRaw.core": "2.1.2" } }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "82SZfdrLe7bdDB8/3INV0UULvlUzsdHkrEYylDCrzFXRWHXG9eO5jJQjRHU8j9XkGIN+MSPgIlczBnqeDvB36A==" + "resolved": "6.0.25", + "contentHash": "i6UpdWqWxSBbIFOkaMoubM40yIjTZO+0rIUkY5JRltSeFI4PzncBBQcNVNXXjAmiLXF/xY0xTS+ykClbkV46Yg==" }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "W7yfdEbEuS1OPPxU0EOA6haqI4uvzs7OwHKh81DiJFn3NFNP2ztSovkOzBDhTwHX0j+OySsAj3BEJhuzTVYIVw==", + "resolved": "6.0.25", + "contentHash": "ci2lR++x7R7LR71+HoeRnB9Z5VeOQ1ILLbFRhsjjWZyLrAMkdq7TK9Ll47jo1TXDWF8Ddeap1JgcptgPKkWSRA==", "dependencies": { - "Microsoft.EntityFrameworkCore": "6.0.22", + "Microsoft.EntityFrameworkCore": "6.0.25", "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" } }, "Microsoft.EntityFrameworkCore.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "xSU77ORQgwlD+s5Cmlk9DzoSCu5oxlHLuQl+v5zAZ0Uv5yH17hp02TBfz3x9nBA+CrIsqaLjGEuyZmLDf/5ATw==", + "resolved": "6.0.25", + "contentHash": "IU4E8I9FS2sUVxJJ0w/4jogLQ8C0zvu/SO6b1tRmiiCtTrHhjUB0tqhxjrFnDXZ/mpCJOElw50+qhbcElm0CYw==", "dependencies": { - "Microsoft.Data.Sqlite.Core": "6.0.22", - "Microsoft.EntityFrameworkCore.Relational": "6.0.22", + "Microsoft.Data.Sqlite.Core": "6.0.25", + "Microsoft.EntityFrameworkCore.Relational": "6.0.25", "Microsoft.Extensions.DependencyModel": "6.0.0" } }, @@ -315,8 +320,8 @@ }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -326,10 +331,10 @@ }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } @@ -472,11 +477,12 @@ "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { + "HL7-dotnetcore": "[2.36.0, )", "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.22, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.25, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[1.0.4, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging": "[1.0.5, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Storage": "[0.2.18, )", "fo-dicom": "[5.1.1, )" } diff --git a/src/Database/Monai.Deploy.InformaticsGateway.Database.csproj b/src/Database/Monai.Deploy.InformaticsGateway.Database.csproj index 2fab0c504..bf29f1d77 100755 --- a/src/Database/Monai.Deploy.InformaticsGateway.Database.csproj +++ b/src/Database/Monai.Deploy.InformaticsGateway.Database.csproj @@ -68,7 +68,11 @@ - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + diff --git a/src/Database/MongoDB/Configurations/DestinationApplicationEntityConfiguration.cs b/src/Database/MongoDB/Configurations/DestinationApplicationEntityConfiguration.cs old mode 100644 new mode 100755 index 25ceefb39..9f1c36e21 --- a/src/Database/MongoDB/Configurations/DestinationApplicationEntityConfiguration.cs +++ b/src/Database/MongoDB/Configurations/DestinationApplicationEntityConfiguration.cs @@ -15,7 +15,7 @@ * limitations under the License. */ -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using MongoDB.Bson.Serialization; namespace Monai.Deploy.InformaticsGateway.Database.MongoDB.Configurations diff --git a/src/Database/MongoDB/Configurations/DicomAssociationInfoConfiguration.cs b/src/Database/MongoDB/Configurations/DicomAssociationInfoConfiguration.cs old mode 100644 new mode 100755 index b5e63d71d..4b6b10034 --- a/src/Database/MongoDB/Configurations/DicomAssociationInfoConfiguration.cs +++ b/src/Database/MongoDB/Configurations/DicomAssociationInfoConfiguration.cs @@ -15,7 +15,7 @@ * limitations under the License. */ -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using MongoDB.Bson.Serialization; namespace Monai.Deploy.InformaticsGateway.Database.MongoDB.Configurations diff --git a/src/Database/MongoDB/Configurations/ExternalAppDetailsConfiguration.cs b/src/Database/MongoDB/Configurations/ExternalAppDetailsConfiguration.cs new file mode 100755 index 000000000..7a6fea639 --- /dev/null +++ b/src/Database/MongoDB/Configurations/ExternalAppDetailsConfiguration.cs @@ -0,0 +1,33 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 Monai.Deploy.InformaticsGateway.Api.Models; +using MongoDB.Bson.Serialization; + +namespace Monai.Deploy.InformaticsGateway.Database.MongoDB.Configurations +{ + internal static class ExternalAppDetailsConfiguration + { + public static void Configure() + { + BsonClassMap.RegisterClassMap(j => + { + j.AutoMap(); + j.SetIgnoreExtraElements(true); + }); + } + } +} diff --git a/src/Database/MongoDB/Configurations/HL7DestinationEntityConfiguration.cs b/src/Database/MongoDB/Configurations/HL7DestinationEntityConfiguration.cs new file mode 100644 index 000000000..a91466b05 --- /dev/null +++ b/src/Database/MongoDB/Configurations/HL7DestinationEntityConfiguration.cs @@ -0,0 +1,34 @@ +/* + * Copyright 2021-2022 MONAI Consortium + * Copyright 2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 Monai.Deploy.InformaticsGateway.Api.Models; +using MongoDB.Bson.Serialization; + +namespace Monai.Deploy.InformaticsGateway.Database.MongoDB.Configurations +{ + internal static class HL7DestinationEntityConfiguration + { + public static void Configure() + { + BsonClassMap.RegisterClassMap(j => + { + j.AutoMap(); + j.SetIgnoreExtraElements(true); + }); + } + } +} diff --git a/src/Database/MongoDB/Configurations/MonaiApplicationEntityConfiguration.cs b/src/Database/MongoDB/Configurations/MonaiApplicationEntityConfiguration.cs old mode 100644 new mode 100755 index 561b0b1f8..3f1eaca0e --- a/src/Database/MongoDB/Configurations/MonaiApplicationEntityConfiguration.cs +++ b/src/Database/MongoDB/Configurations/MonaiApplicationEntityConfiguration.cs @@ -15,7 +15,7 @@ * limitations under the License. */ -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using MongoDB.Bson.Serialization; namespace Monai.Deploy.InformaticsGateway.Database.MongoDB.Configurations diff --git a/src/Database/MongoDB/Configurations/MongoDBEntityBaseConfiguration.cs b/src/Database/MongoDB/Configurations/MongoDBEntityBaseConfiguration.cs old mode 100644 new mode 100755 index c7ee5ac3d..9abd8b535 --- a/src/Database/MongoDB/Configurations/MongoDBEntityBaseConfiguration.cs +++ b/src/Database/MongoDB/Configurations/MongoDBEntityBaseConfiguration.cs @@ -15,7 +15,7 @@ * limitations under the License. */ -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Storage; using MongoDB.Bson.Serialization; namespace Monai.Deploy.InformaticsGateway.Database.MongoDB.Configurations diff --git a/src/Database/MongoDB/Integration.Test/DestinationApplicationEntityRepositoryTest.cs b/src/Database/MongoDB/Integration.Test/DestinationApplicationEntityRepositoryTest.cs index f4628ef67..522e19023 100755 --- a/src/Database/MongoDB/Integration.Test/DestinationApplicationEntityRepositoryTest.cs +++ b/src/Database/MongoDB/Integration.Test/DestinationApplicationEntityRepositoryTest.cs @@ -18,7 +18,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.EntityFramework.Test; using Monai.Deploy.InformaticsGateway.Database.MongoDB.Repositories; diff --git a/src/Database/MongoDB/Integration.Test/DicomAssociationInfoRepositoryTest.cs b/src/Database/MongoDB/Integration.Test/DicomAssociationInfoRepositoryTest.cs index 3dd049a1f..919693090 100755 --- a/src/Database/MongoDB/Integration.Test/DicomAssociationInfoRepositoryTest.cs +++ b/src/Database/MongoDB/Integration.Test/DicomAssociationInfoRepositoryTest.cs @@ -18,7 +18,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.EntityFramework.Test; using Monai.Deploy.InformaticsGateway.Database.MongoDB.Repositories; diff --git a/src/Database/MongoDB/Integration.Test/ExternalAppDetailsRepositoryTest.cs b/src/Database/MongoDB/Integration.Test/ExternalAppDetailsRepositoryTest.cs new file mode 100755 index 000000000..e2755cfce --- /dev/null +++ b/src/Database/MongoDB/Integration.Test/ExternalAppDetailsRepositoryTest.cs @@ -0,0 +1,136 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api.Models; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.EntityFramework.Test; +using Monai.Deploy.InformaticsGateway.Database.MongoDB.Repositories; +using MongoDB.Driver; +using Moq; + + +namespace Monai.Deploy.InformaticsGateway.Database.MongoDB.Integration.Test +{ + [Collection("MongoDatabase")] + public class ExternalAppDetailsRepositoryTest + { + private readonly MongoDatabaseFixture _databaseFixture; + + private readonly Mock _serviceScopeFactory; + private readonly Mock> _logger; + private readonly IOptions _options; + + private readonly Mock _serviceScope; + private readonly IServiceProvider _serviceProvider; + + public ExternalAppDetailsRepositoryTest(MongoDatabaseFixture databaseFixture) + { + _databaseFixture = databaseFixture ?? throw new ArgumentNullException(nameof(databaseFixture)); + _databaseFixture.InitDatabaseWithExternalAppEntities(); + + _serviceScopeFactory = new Mock(); + _logger = new Mock>(); + _options = _databaseFixture.Options; + + _serviceScope = new Mock(); + var services = new ServiceCollection(); + services.AddScoped(p => _logger.Object); + services.AddScoped(p => databaseFixture.Client); + + _serviceProvider = services.BuildServiceProvider(); + _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); + _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); + + _options.Value.Retries.DelaysMilliseconds = new[] { 1, 1, 1 }; + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + } + + [Fact] + public async Task GivenExternalAppDetailsEntitiesInTheDatabase_WhenGetAsyncCalled_ExpectEntitieToBeReturned() + { + var store = new ExternalAppDetailsRepository(_serviceScopeFactory.Object, _logger.Object, _databaseFixture.Options); + + var collection = _databaseFixture.Database.GetCollection(nameof(ExternalAppDetails)); + + var expected = (await collection.FindAsync(e => e.StudyInstanceUid == "2").ConfigureAwait(false)).First(); + var actual = (await store.GetAsync("2", new CancellationToken()).ConfigureAwait(false)).FirstOrDefault(); + + actual.Should().NotBeNull(); + Assert.Equal(expected.StudyInstanceUid, actual!.StudyInstanceUid); + Assert.Equal(expected.ExportTaskID, actual!.ExportTaskID); + Assert.Equal(expected.CorrelationId, actual!.CorrelationId); + Assert.Equal(expected.WorkflowInstanceId, actual!.WorkflowInstanceId); + } + + [Fact] + public async Task GivenAExternalAppDetails_WhenAddingToDatabase_ExpectItToBeSaved() + { + var app = new ExternalAppDetails { StudyInstanceUid = "3", ExportTaskID = "ExportTaskID3", CorrelationId = "CorrelationId3", WorkflowInstanceId = "WorkflowInstanceId3" }; + + var store = new ExternalAppDetailsRepository(_serviceScopeFactory.Object, _logger.Object, _options); + await store.AddAsync(app, new CancellationToken()).ConfigureAwait(false); + + var collection = _databaseFixture.Database.GetCollection(nameof(ExternalAppDetails)); + var actual = await collection.Find(p => p.Id == app.Id).FirstOrDefaultAsync().ConfigureAwait(false); + + Assert.NotNull(actual); + Assert.Equal(app.StudyInstanceUid, actual!.StudyInstanceUid); + Assert.Equal(app.ExportTaskID, actual!.ExportTaskID); + Assert.Equal(app.CorrelationId, actual!.CorrelationId); + Assert.Equal(app.WorkflowInstanceId, actual!.WorkflowInstanceId); + + actual!.DateTimeCreated.Should().BeCloseTo(app.DateTimeCreated, TimeSpan.FromMilliseconds(500)); + } + + [Fact] + public async Task GivenExternalAppDetailsEntitiesInTheDatabase_WhenGetPatientOutboundAsyncCalled_ExpectEntitieToBeReturned() + { + var store = new ExternalAppDetailsRepository(_serviceScopeFactory.Object, _logger.Object, _databaseFixture.Options); + + var collection = _databaseFixture.Database.GetCollection(nameof(ExternalAppDetails)); + + var expected = (await collection.FindAsync(e => e.PatientIdOutBound == "pat1out1").ConfigureAwait(false)).First(); + var actual = (await store.GetByPatientIdOutboundAsync("pat1out1", new CancellationToken()).ConfigureAwait(false)); + + actual.Should().NotBeNull(); + Assert.Equal(expected.StudyInstanceUid, actual!.StudyInstanceUid); + Assert.Equal(expected.ExportTaskID, actual!.ExportTaskID); + Assert.Equal(expected.CorrelationId, actual!.CorrelationId); + Assert.Equal(expected.WorkflowInstanceId, actual!.WorkflowInstanceId); + } + + [Fact] + public async Task GivenExternalAppDetailsEntitiesInTheDatabase_WhenGetStudyIdOutboundAsyncCalled_ExpectEntitieToBeReturned() + { + var store = new ExternalAppDetailsRepository(_serviceScopeFactory.Object, _logger.Object, _databaseFixture.Options); + + var collection = _databaseFixture.Database.GetCollection(nameof(ExternalAppDetails)); + + var expected = (await collection.FindAsync(e => e.StudyInstanceUidOutBound == "sudIdOut2").ConfigureAwait(false)).First(); + var actual = (await store.GetByStudyIdOutboundAsync("sudIdOut2", new CancellationToken()).ConfigureAwait(false)); + + actual.Should().NotBeNull(); + Assert.Equal(expected.StudyInstanceUid, actual!.StudyInstanceUid); + Assert.Equal(expected.ExportTaskID, actual!.ExportTaskID); + Assert.Equal(expected.CorrelationId, actual!.CorrelationId); + Assert.Equal(expected.WorkflowInstanceId, actual!.WorkflowInstanceId); + } + } +} diff --git a/src/Database/MongoDB/Integration.Test/HL7DestinationEntityRepositoryTest.cs b/src/Database/MongoDB/Integration.Test/HL7DestinationEntityRepositoryTest.cs new file mode 100644 index 000000000..dbdb29251 --- /dev/null +++ b/src/Database/MongoDB/Integration.Test/HL7DestinationEntityRepositoryTest.cs @@ -0,0 +1,166 @@ +/* + * Copyright 2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api.Models; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.EntityFramework.Test; +using Monai.Deploy.InformaticsGateway.Database.MongoDB.Repositories; +using MongoDB.Driver; +using Moq; + + +namespace Monai.Deploy.InformaticsGateway.Database.MongoDB.Integration.Test +{ + [Collection("MongoDatabase")] + public class HL7DestinationEntityRepositoryTest + { + private readonly MongoDatabaseFixture _databaseFixture; + + private readonly Mock _serviceScopeFactory; + private readonly Mock> _logger; + private readonly IOptions _options; + + private readonly Mock _serviceScope; + private readonly IServiceProvider _serviceProvider; + + public HL7DestinationEntityRepositoryTest(MongoDatabaseFixture databaseFixture) + { + _databaseFixture = databaseFixture ?? throw new ArgumentNullException(nameof(databaseFixture)); + _databaseFixture.InitDatabaseWithHL7DestinationEntities(); + + _serviceScopeFactory = new Mock(); + _logger = new Mock>(); + _options = Options.Create(new DatabaseOptions()); + + _serviceScope = new Mock(); + var services = new ServiceCollection(); + services.AddScoped(p => _logger.Object); + services.AddScoped(p => databaseFixture.Client); + + _serviceProvider = services.BuildServiceProvider(); + _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); + _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); + + _options.Value.Retries.DelaysMilliseconds = new[] { 1, 1, 1 }; + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + } + + [Fact] + public async Task GivenAHL7DestinationEntity_WhenAddingToDatabase_ExpectItToBeSaved() + { + var aet = new HL7DestinationEntity { AeTitle = "AET", HostIp = "1.2.3.4", Port = 114, Name = "AET" }; + + var store = new HL7DestinationEntityRepository(_serviceScopeFactory.Object, _logger.Object, _options, _databaseFixture.Options); + await store.AddAsync(aet).ConfigureAwait(false); + + var collection = _databaseFixture.Database.GetCollection(nameof(HL7DestinationEntity)); + var actual = await collection.Find(p => p.Name == aet.Name).FirstOrDefaultAsync().ConfigureAwait(false); + + Assert.NotNull(actual); + Assert.Equal(aet.AeTitle, actual!.AeTitle); + Assert.Equal(aet.HostIp, actual!.HostIp); + Assert.Equal(aet.Port, actual!.Port); + Assert.Equal(aet.Name, actual!.Name); + } + + [Fact] + public async Task GivenAExpressionFilter_WhenContainsAsyncIsCalled_ExpectItToReturnMatchingObjects() + { + var store = new HL7DestinationEntityRepository(_serviceScopeFactory.Object, _logger.Object, _options, _databaseFixture.Options); + + var result = await store.ContainsAsync(p => p.AeTitle == "AET1").ConfigureAwait(false); + Assert.True(result); + result = await store.ContainsAsync(p => p.AeTitle.Equals("AET1")).ConfigureAwait(false); + Assert.True(result); + result = await store.ContainsAsync(p => p.AeTitle != "AET1" && p.Port == 114 && p.HostIp == "1.2.3.4").ConfigureAwait(false); + Assert.True(result); + result = await store.ContainsAsync(p => p.Port == 114 && p.HostIp == "1.2.3.4").ConfigureAwait(false); + Assert.True(result); + result = await store.ContainsAsync(p => p.Port == 999).ConfigureAwait(false); + Assert.False(result); + } + + [Fact] + public async Task GivenAAETitleName_WhenFindByNameAsyncIsCalled_ExpectItToReturnMatchingEntity() + { + var store = new HL7DestinationEntityRepository(_serviceScopeFactory.Object, _logger.Object, _options, _databaseFixture.Options); + + var actual = await store.FindByNameAsync("AET1").ConfigureAwait(false); + Assert.NotNull(actual); + Assert.Equal("AET1", actual!.AeTitle); + Assert.Equal("1.2.3.4", actual!.HostIp); + Assert.Equal(114, actual!.Port); + Assert.Equal("AET1", actual!.Name); + + actual = await store.FindByNameAsync("AET6").ConfigureAwait(false); + Assert.Null(actual); + } + + [Fact] + public async Task GivenAHL7DestinationEntity_WhenRemoveIsCalled_ExpectItToDeleted() + { + var store = new HL7DestinationEntityRepository(_serviceScopeFactory.Object, _logger.Object, _options, _databaseFixture.Options); + + var expected = await store.FindByNameAsync("AET5").ConfigureAwait(false); + Assert.NotNull(expected); + + var actual = await store.RemoveAsync(expected!).ConfigureAwait(false); + Assert.Same(expected, actual); + + var collection = _databaseFixture.Database.GetCollection(nameof(HL7DestinationEntity)); + var dbResult = await collection.Find(p => p.Name == "AET5").FirstOrDefaultAsync().ConfigureAwait(false); + Assert.Null(dbResult); + } + + [Fact] + public async Task GivenHL7DestinationEntitiesInTheDatabase_WhenToListIsCalled_ExpectAllEntitiesToBeReturned() + { + var store = new HL7DestinationEntityRepository(_serviceScopeFactory.Object, _logger.Object, _options, _databaseFixture.Options); + + var collection = _databaseFixture.Database.GetCollection(nameof(HL7DestinationEntity)); + var expected = await collection.Find(Builders.Filter.Empty).ToListAsync().ConfigureAwait(false); + var actual = await store.ToListAsync().ConfigureAwait(false); + + actual.Should().BeEquivalentTo(expected, options => options.Excluding(p => p.DateTimeCreated)); + } + + [Fact] + public async Task GivenAHL7DestinationEntity_WhenUpdatedIsCalled_ExpectItToSaved() + { + var store = new HL7DestinationEntityRepository(_serviceScopeFactory.Object, _logger.Object, _options, _databaseFixture.Options); + + var expected = await store.FindByNameAsync("AET3").ConfigureAwait(false); + Assert.NotNull(expected); + + expected!.AeTitle = "AET100"; + expected!.Port = 1000; + expected!.HostIp = "loalhost"; + + var actual = await store.UpdateAsync(expected).ConfigureAwait(false); + Assert.Equal(expected, actual); + + var dbResult = await store.FindByNameAsync("AET3").ConfigureAwait(false); + Assert.NotNull(dbResult); + Assert.Equal(expected.AeTitle, dbResult!.AeTitle); + Assert.Equal(expected.HostIp, dbResult!.HostIp); + Assert.Equal(expected.Port, dbResult!.Port); + } + } +} diff --git a/src/Database/MongoDB/Integration.Test/MonaiApplicationEntityRepositoryTest.cs b/src/Database/MongoDB/Integration.Test/MonaiApplicationEntityRepositoryTest.cs index e8da9cec8..5c73132a9 100755 --- a/src/Database/MongoDB/Integration.Test/MonaiApplicationEntityRepositoryTest.cs +++ b/src/Database/MongoDB/Integration.Test/MonaiApplicationEntityRepositoryTest.cs @@ -18,7 +18,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.EntityFramework.Test; using Monai.Deploy.InformaticsGateway.Database.MongoDB.Repositories; diff --git a/src/Database/MongoDB/Integration.Test/MongoDatabaseFixture.cs b/src/Database/MongoDB/Integration.Test/MongoDatabaseFixture.cs index f8645bead..f1d48885e 100755 --- a/src/Database/MongoDB/Integration.Test/MongoDatabaseFixture.cs +++ b/src/Database/MongoDB/Integration.Test/MongoDatabaseFixture.cs @@ -16,6 +16,7 @@ using Microsoft.Extensions.Options; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Api.Rest; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.MongoDB; @@ -64,6 +65,23 @@ public void InitDatabaseWithDestinationApplicationEntities() collection.InsertOne(aet5); } + public void InitDatabaseWithHL7DestinationEntities() + { + var collection = Database.GetCollection(nameof(HL7DestinationEntity)); + Clear(collection); + var aet1 = new HL7DestinationEntity { AeTitle = "AET1", HostIp = "1.2.3.4", Port = 114, Name = "AET1", DateTimeCreated = DateTime.UtcNow }; + var aet2 = new HL7DestinationEntity { AeTitle = "AET2", HostIp = "1.2.3.4", Port = 114, Name = "AET2", DateTimeCreated = DateTime.UtcNow }; + var aet3 = new HL7DestinationEntity { AeTitle = "AET3", HostIp = "1.2.3.4", Port = 114, Name = "AET3", DateTimeCreated = DateTime.UtcNow }; + var aet4 = new HL7DestinationEntity { AeTitle = "AET4", HostIp = "1.2.3.4", Port = 114, Name = "AET4", DateTimeCreated = DateTime.UtcNow }; + var aet5 = new HL7DestinationEntity { AeTitle = "AET5", HostIp = "1.2.3.4", Port = 114, Name = "AET5", DateTimeCreated = DateTime.UtcNow }; + + collection.InsertOne(aet1); + collection.InsertOne(aet2); + collection.InsertOne(aet3); + collection.InsertOne(aet4); + collection.InsertOne(aet5); + } + public void InitDatabaseWithMonaiApplicationEntities() { var collection = Database.GetCollection(nameof(MonaiApplicationEntity)); @@ -114,6 +132,17 @@ public void InitDatabaseWithSourceApplicationEntities() collection.InsertOne(aet4); collection.InsertOne(aet5); } + public void InitDatabaseWithExternalAppEntities() + { + var collection = Database.GetCollection(nameof(ExternalAppDetails)); + Clear(collection); + + var ea1 = new ExternalAppDetails { StudyInstanceUid = "1", ExportTaskID = "ExportTaskID", CorrelationId = "CorrelationId", WorkflowInstanceId = "WorkflowInstanceId", PatientIdOutBound = "pat1out1", StudyInstanceUidOutBound = "sudIdOut1" }; + var ea2 = new ExternalAppDetails { StudyInstanceUid = "2", ExportTaskID = "ExportTaskID2", CorrelationId = "CorrelationId2", WorkflowInstanceId = "WorkflowInstanceId2", PatientIdOutBound = "pat1out2", StudyInstanceUidOutBound = "sudIdOut2" }; + + collection.InsertOne(ea1); + collection.InsertOne(ea2); + } public void InitDatabaseWithInferenceRequests() { diff --git a/src/Database/MongoDB/Integration.Test/packages.lock.json b/src/Database/MongoDB/Integration.Test/packages.lock.json index 3a2a4784a..c0cbcf5cf 100755 --- a/src/Database/MongoDB/Integration.Test/packages.lock.json +++ b/src/Database/MongoDB/Integration.Test/packages.lock.json @@ -110,6 +110,11 @@ "System.Threading.Channels": "6.0.0" } }, + "HL7-dotnetcore": { + "type": "Transitive", + "resolved": "2.36.0", + "contentHash": "N1HLMeIqYuY+4O69ItgZJoDBnnpNkK5N2pClceTJ2nFJxsP48iCsA4iz3tm43Yszi4r/vaThoc3UoLBfGP3vKw==" + }, "Macross.Json.Extensions": { "type": "Transitive", "resolved": "3.0.0", @@ -132,8 +137,8 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", @@ -274,8 +279,8 @@ }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -285,10 +290,10 @@ }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } @@ -1395,11 +1400,12 @@ "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { + "HL7-dotnetcore": "[2.36.0, )", "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.22, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.25, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[1.0.4, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging": "[1.0.5, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Storage": "[0.2.18, )", "fo-dicom": "[5.1.1, )" } diff --git a/src/Database/MongoDB/MongoDatabaseMigrationManager.cs b/src/Database/MongoDB/MongoDatabaseMigrationManager.cs old mode 100644 new mode 100755 index f9f4f2d40..151282bad --- a/src/Database/MongoDB/MongoDatabaseMigrationManager.cs +++ b/src/Database/MongoDB/MongoDatabaseMigrationManager.cs @@ -40,6 +40,7 @@ public IHost Migrate(IHost host) StorageMetadataWrapperEntityConfiguration.Configure(); DicomAssociationInfoConfiguration.Configure(); VirtualApplicationEntityConfiguration.Configure(); + ExternalAppDetailsConfiguration.Configure(); return host; } } diff --git a/src/Database/MongoDB/Repositories/DestinationApplicationEntityRepository.cs b/src/Database/MongoDB/Repositories/DestinationApplicationEntityRepository.cs index b00d3e7d3..85c071e24 100755 --- a/src/Database/MongoDB/Repositories/DestinationApplicationEntityRepository.cs +++ b/src/Database/MongoDB/Repositories/DestinationApplicationEntityRepository.cs @@ -19,7 +19,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.Api; using Monai.Deploy.InformaticsGateway.Database.Api.Logging; diff --git a/src/Database/MongoDB/Repositories/DicomAssociationInfoRepository.cs b/src/Database/MongoDB/Repositories/DicomAssociationInfoRepository.cs index f81a18d6f..1056bc0c1 100755 --- a/src/Database/MongoDB/Repositories/DicomAssociationInfoRepository.cs +++ b/src/Database/MongoDB/Repositories/DicomAssociationInfoRepository.cs @@ -18,7 +18,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.Api.Logging; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; diff --git a/src/Database/MongoDB/Repositories/ExternalAppDetailsRepository.cs b/src/Database/MongoDB/Repositories/ExternalAppDetailsRepository.cs new file mode 100755 index 000000000..16a4a7feb --- /dev/null +++ b/src/Database/MongoDB/Repositories/ExternalAppDetailsRepository.cs @@ -0,0 +1,139 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 Ardalis.GuardClauses; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api.Models; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api.Logging; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using MongoDB.Driver; +using Polly; +using Polly.Retry; + +namespace Monai.Deploy.InformaticsGateway.Database.MongoDB.Repositories +{ + public class ExternalAppDetailsRepository : IExternalAppDetailsRepository, IDisposable + { + private readonly ILogger _logger; + private readonly IServiceScope _scope; + private readonly AsyncRetryPolicy _retryPolicy; + private readonly IMongoCollection _collection; + private bool _disposedValue; + + public ExternalAppDetailsRepository( + IServiceScopeFactory serviceScopeFactory, + ILogger logger, + IOptions options) + { + Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); + Guard.Against.Null(options, nameof(options)); + + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _scope = serviceScopeFactory.CreateScope(); + _retryPolicy = Policy.Handle().WaitAndRetryAsync( + options.Value.Retries.RetryDelays, + (exception, timespan, count, context) => _logger.DatabaseErrorRetry(timespan, count, exception)); + + var mongoDbClient = _scope.ServiceProvider.GetRequiredService(); + var mongoDatabase = mongoDbClient.GetDatabase(options.Value.DatabaseName); + _collection = mongoDatabase.GetCollection(nameof(ExternalAppDetails)); + CreateIndexes(); + } + + private void CreateIndexes() + { + var indexDefinitionState = Builders.IndexKeys + .Ascending(_ => _.StudyInstanceUid); + _collection.Indexes.CreateOne(new CreateIndexModel(indexDefinitionState)); + + indexDefinitionState = Builders.IndexKeys + .Ascending(_ => _.PatientIdOutBound); + _collection.Indexes.CreateOne(new CreateIndexModel(indexDefinitionState)); + + indexDefinitionState = Builders.IndexKeys + .Ascending(_ => _.StudyInstanceUidOutBound); + _collection.Indexes.CreateOne(new CreateIndexModel(indexDefinitionState)); + + var options = new CreateIndexOptions { ExpireAfter = TimeSpan.FromDays(7), Name = "DateTimeCreated" }; + indexDefinitionState = Builders.IndexKeys.Ascending(_ => _.DateTimeCreated); + _collection.Indexes.CreateOne(new CreateIndexModel(indexDefinitionState, options)); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _scope.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public async Task AddAsync(ExternalAppDetails details, CancellationToken cancellationToken) + { + Guard.Against.Null(details, nameof(details)); + + await _retryPolicy.ExecuteAsync(async () => + { + await _collection.InsertOneAsync(details, cancellationToken: cancellationToken).ConfigureAwait(false); + }).ConfigureAwait(false); + } + + public async Task> GetAsync(string studyInstanceId, CancellationToken cancellationToken) + { + return await _retryPolicy.ExecuteAsync(async () => + { + return (await _collection.FindAsync(p => + p.StudyInstanceUid == studyInstanceId, null, cancellationToken + ).ConfigureAwait(false)).ToList(); + }).ConfigureAwait(false); + } + + public async Task GetByPatientIdOutboundAsync(string patientId, CancellationToken cancellationToken) + { + return await _retryPolicy.ExecuteAsync(async () => + { + return (await _collection.FindAsync(p => + p.PatientIdOutBound == patientId, null, cancellationToken + ).ConfigureAwait(false)).FirstOrDefault(); + }).ConfigureAwait(false); + } + + public async Task GetByStudyIdOutboundAsync(string studyInstanceId, CancellationToken cancellationToken) + { + return await _retryPolicy.ExecuteAsync(async () => + { + return (await _collection.FindAsync(p => + p.StudyInstanceUidOutBound == studyInstanceId, null, cancellationToken + ).ConfigureAwait(false)).FirstOrDefault(); + }).ConfigureAwait(false); + } + } +} diff --git a/src/Database/MongoDB/Repositories/HL7DestinationEntityRepository.cs b/src/Database/MongoDB/Repositories/HL7DestinationEntityRepository.cs new file mode 100755 index 000000000..feb428f09 --- /dev/null +++ b/src/Database/MongoDB/Repositories/HL7DestinationEntityRepository.cs @@ -0,0 +1,173 @@ +/* + * Copyright 2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.Linq.Expressions; +using Ardalis.GuardClauses; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api.Models; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api; +using Monai.Deploy.InformaticsGateway.Database.Api.Logging; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using MongoDB.Driver; +using Polly; +using Polly.Retry; + +namespace Monai.Deploy.InformaticsGateway.Database.MongoDB.Repositories +{ + public class HL7DestinationEntityRepository : IHL7DestinationEntityRepository, IDisposable + { + private readonly ILogger _logger; + private readonly IServiceScope _scope; + private readonly AsyncRetryPolicy _retryPolicy; + private readonly IMongoCollection _collection; + private bool _disposedValue; + + public HL7DestinationEntityRepository( + IServiceScopeFactory serviceScopeFactory, + ILogger logger, + IOptions options, + IOptions mongoDbOptions) + { + Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); + Guard.Against.Null(options, nameof(options)); + Guard.Against.Null(mongoDbOptions, nameof(mongoDbOptions)); + + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _scope = serviceScopeFactory.CreateScope(); + _retryPolicy = Policy.Handle().WaitAndRetryAsync( + options.Value.Retries.RetryDelays, + (exception, timespan, count, context) => + { + _logger.DatabaseErrorRetry(timespan, count, exception); + }); + + var mongoDbClient = _scope.ServiceProvider.GetRequiredService(); + var mongoDatabase = mongoDbClient.GetDatabase(mongoDbOptions.Value.DatabaseName); + _collection = mongoDatabase.GetCollection(nameof(HL7DestinationEntity)); + CreateIndexes(); + } + + private void CreateIndexes() + { + var options = new CreateIndexOptions { Unique = true }; + + var indexDefinition = Builders.IndexKeys + .Ascending(_ => _.Name); + _collection.Indexes.CreateOne(new CreateIndexModel(indexDefinition, options)); + + var indexDefinitionAll = Builders.IndexKeys.Combine( + Builders.IndexKeys.Ascending(_ => _.Name), + Builders.IndexKeys.Ascending(_ => _.AeTitle), + Builders.IndexKeys.Ascending(_ => _.HostIp), + Builders.IndexKeys.Ascending(_ => _.Port)); + _collection.Indexes.CreateOne(new CreateIndexModel(indexDefinitionAll, options)); + } + + public async Task> ToListAsync(CancellationToken cancellationToken = default) + { + return await _retryPolicy.ExecuteAsync(async () => + { + return await _collection.Find(Builders.Filter.Empty).ToListAsync(cancellationToken).ConfigureAwait(false); + }).ConfigureAwait(false); + } + + public async Task FindByNameAsync(string name, CancellationToken cancellationToken = default) + { + Guard.Against.NullOrWhiteSpace(name, nameof(name)); + + return await _retryPolicy.ExecuteAsync(async () => + { + return await _collection + .Find(x => x.Name == name) + .FirstOrDefaultAsync().ConfigureAwait(false); + }).ConfigureAwait(false); + } + + public async Task AddAsync(HL7DestinationEntity item, CancellationToken cancellationToken = default) + { + Guard.Against.Null(item, nameof(item)); + + return await _retryPolicy.ExecuteAsync(async () => + { + await _collection.InsertOneAsync(item, cancellationToken: cancellationToken).ConfigureAwait(false); + return item; + }).ConfigureAwait(false); + } + + public async Task UpdateAsync(HL7DestinationEntity entity, CancellationToken cancellationToken = default) + { + Guard.Against.Null(entity, nameof(entity)); + + return await _retryPolicy.ExecuteAsync(async () => + { + var result = await _collection.ReplaceOneAsync(p => p.Id == entity.Id, entity, cancellationToken: cancellationToken).ConfigureAwait(false); + if (result.ModifiedCount == 0) + { + throw new DatabaseException("Failed to update entity"); + } + return entity; + }).ConfigureAwait(false); + } + + public async Task RemoveAsync(HL7DestinationEntity entity, CancellationToken cancellationToken = default) + { + Guard.Against.Null(entity, nameof(entity)); + + return await _retryPolicy.ExecuteAsync(async () => + { + var result = await _collection.DeleteOneAsync(Builders.Filter.Where(p => p.Name == entity.Name), cancellationToken: cancellationToken).ConfigureAwait(false); + if (result.DeletedCount == 0) + { + throw new DatabaseException("Failed to delete entity"); + } + return entity; + }).ConfigureAwait(false); + } + + public async Task ContainsAsync(Expression> predicate, CancellationToken cancellationToken = default) + { + return await _retryPolicy.ExecuteAsync(async () => + { + var result = await _collection.FindAsync(predicate, cancellationToken: cancellationToken).ConfigureAwait(false); + return await result.AnyAsync(cancellationToken).ConfigureAwait(false); + }).ConfigureAwait(false); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _scope.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Database/MongoDB/Repositories/Hl7ApplicationConfigRepository.cs b/src/Database/MongoDB/Repositories/Hl7ApplicationConfigRepository.cs new file mode 100755 index 000000000..8b23cf257 --- /dev/null +++ b/src/Database/MongoDB/Repositories/Hl7ApplicationConfigRepository.cs @@ -0,0 +1,152 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 Ardalis.GuardClauses; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api; +using Monai.Deploy.InformaticsGateway.Database.Api.Logging; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using MongoDB.Driver; +using Polly; +using Polly.Retry; + +namespace Monai.Deploy.InformaticsGateway.Database.MongoDB.Repositories +{ + public class Hl7ApplicationConfigRepository : IHl7ApplicationConfigRepository, IDisposable + { + private readonly ILogger _logger; + private readonly IServiceScope _scope; + private readonly AsyncRetryPolicy _retryPolicy; + private readonly IMongoCollection _collection; + private bool _disposedValue; + + public Hl7ApplicationConfigRepository(IServiceScopeFactory serviceScopeFactory, + ILogger logger, + IOptions options) + { + Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); + Guard.Against.Null(options, nameof(options)); + + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _scope = serviceScopeFactory.CreateScope(); + _retryPolicy = Policy.Handle().WaitAndRetryAsync( + options.Value.Retries.RetryDelays, + (exception, timespan, count, context) => + { + _logger.DatabaseErrorRetry(timespan, count, exception); + }); + + var mongoDbClient = _scope.ServiceProvider.GetRequiredService(); + var mongoDatabase = mongoDbClient.GetDatabase(options.Value.DatabaseName); + _collection = mongoDatabase.GetCollection(nameof(Hl7ApplicationConfigEntity)); + CreateIndexes(); + } + + private void CreateIndexes() + { + var options = new CreateIndexOptions { Unique = true }; + + var indexDefinition = Builders.IndexKeys + .Ascending(_ => _.DateTimeCreated); + _collection.Indexes.CreateOne(new CreateIndexModel(indexDefinition, options)); + } + + public Task> GetAllAsync(CancellationToken cancellationToken = default) => + _retryPolicy.ExecuteAsync(() => + _collection.Find(Builders.Filter.Empty).ToListAsync(cancellationToken)); + + public Task GetByIdAsync(string id) + { + var GuidId = Guid.Parse(id); + return _retryPolicy.ExecuteAsync(() => _collection + .Find(x => x.Id == GuidId) + .FirstOrDefaultAsync())!; + } + + public Task DeleteAsync(string id, CancellationToken cancellationToken = default) + { + return _retryPolicy.ExecuteAsync(async () => + { + var entity = await GetByIdAsync(id).ConfigureAwait(false) ?? throw new DatabaseException("Failed to delete entity."); + + var result = await _collection + .DeleteOneAsync(Builders.Filter.Where(p => p.Id == entity.Id), + cancellationToken: cancellationToken).ConfigureAwait(false); + + if (result.DeletedCount == 0) + { + throw new DatabaseException("Failed to delete entity"); + } + + return entity; + }); + } + + public Task CreateAsync(Hl7ApplicationConfigEntity configEntity, + CancellationToken cancellationToken = default) + { + return _retryPolicy.ExecuteAsync(async () => + { + await _collection.InsertOneAsync(configEntity, cancellationToken: cancellationToken) + .ConfigureAwait(false); + return configEntity; + }); + } + + public Task UpdateAsync(Hl7ApplicationConfigEntity configEntity, + CancellationToken cancellationToken = default) + { + + return _retryPolicy.ExecuteAsync(async () => + { + var result = await _collection + .ReplaceOneAsync(Builders.Filter.Where(p => p.Id == configEntity.Id), + configEntity, cancellationToken: cancellationToken).ConfigureAwait(false); + if (result.ModifiedCount == 0) + { + throw new DatabaseException("Failed to update entity"); + } + + return configEntity; + })!; + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _scope.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Database/MongoDB/Repositories/MonaiApplicationEntityRepository.cs b/src/Database/MongoDB/Repositories/MonaiApplicationEntityRepository.cs index 87cc1ace1..0f9ecdb36 100755 --- a/src/Database/MongoDB/Repositories/MonaiApplicationEntityRepository.cs +++ b/src/Database/MongoDB/Repositories/MonaiApplicationEntityRepository.cs @@ -19,7 +19,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.Api; using Monai.Deploy.InformaticsGateway.Database.Api.Logging; diff --git a/src/Database/MongoDB/packages.lock.json b/src/Database/MongoDB/packages.lock.json index cc8a764f1..cd453a7c0 100755 --- a/src/Database/MongoDB/packages.lock.json +++ b/src/Database/MongoDB/packages.lock.json @@ -69,6 +69,11 @@ "System.Threading.Channels": "6.0.0" } }, + "HL7-dotnetcore": { + "type": "Transitive", + "resolved": "2.36.0", + "contentHash": "N1HLMeIqYuY+4O69ItgZJoDBnnpNkK5N2pClceTJ2nFJxsP48iCsA4iz3tm43Yszi4r/vaThoc3UoLBfGP3vKw==" + }, "Macross.Json.Extensions": { "type": "Transitive", "resolved": "3.0.0", @@ -86,8 +91,8 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", @@ -195,8 +200,8 @@ }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -206,10 +211,10 @@ }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } @@ -373,11 +378,12 @@ "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { + "HL7-dotnetcore": "[2.36.0, )", "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.22, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.25, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[1.0.4, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging": "[1.0.5, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Storage": "[0.2.18, )", "fo-dicom": "[5.1.1, )" } diff --git a/src/Database/packages.lock.json b/src/Database/packages.lock.json index 527ebfc3c..af4bbeb2a 100755 --- a/src/Database/packages.lock.json +++ b/src/Database/packages.lock.json @@ -12,15 +12,24 @@ "MongoDB.Driver": "2.14.1" } }, + "Microsoft.EntityFrameworkCore.Tools": { + "type": "Direct", + "requested": "[6.0.25, )", + "resolved": "6.0.25", + "contentHash": "2iPMR+DHXh2Xn9qoJ0ejzdHblpns73e1pZ/pyRbYDQi0HPJyq1/pTYDda1owJ5W2lxAGDg8l5Fl1jVp97fTR1g==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Design": "6.0.25" + } + }, "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": { "type": "Direct", - "requested": "[6.0.22, )", - "resolved": "6.0.22", - "contentHash": "PNj+/e/GCJh3ZNzxEGhkMpKJgmmbuGar6Uk/R3mPFZacTx6lBdLs4Ev7uf4XQWqTdJe56rK+2P3oF/9jIGbxgw==", + "requested": "[6.0.25, )", + "resolved": "6.0.25", + "contentHash": "Cmhq0sgb53+dh9xHOlBEQUhi13vsZeQ4fcYC9JYO4med7pabj9x3100opCdUv+7UX+tUC1GPm/nco+1skJdLFA==", "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "6.0.22", - "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.22", - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.22" + "Microsoft.EntityFrameworkCore.Relational": "6.0.25", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.25", + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.25" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { @@ -85,6 +94,16 @@ "System.Threading.Channels": "6.0.0" } }, + "HL7-dotnetcore": { + "type": "Transitive", + "resolved": "2.36.0", + "contentHash": "N1HLMeIqYuY+4O69ItgZJoDBnnpNkK5N2pClceTJ2nFJxsP48iCsA4iz3tm43Yszi4r/vaThoc3UoLBfGP3vKw==" + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.8.26", + "contentHash": "OiKusGL20vby4uDEswj2IgkdchC1yQ6rwbIkZDVBPIR6al2b7n3pC91elBul9q33KaBgRKhbZH3+2Ur4fnWx2A==" + }, "Macross.Json.Extensions": { "type": "Transitive", "resolved": "3.0.0", @@ -102,19 +121,19 @@ }, "Microsoft.Data.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "gtIGHbGnRq/h4mFSJYr9BdMObvJV/a67nBubs50VjPDusQARtWJzeVTirDWsbL1qTvGzbbZCD7VE7+s2ixZfow==", + "resolved": "6.0.25", + "contentHash": "rbXNoMg/ylGyJxLcyetojuXFzvDG85M31DfFbqL8veN4P8oG6wmnPwWNn3/bDIEDVvdw15R092dxpobQeQcjGg==", "dependencies": { "SQLitePCLRaw.core": "2.1.2" } }, "Microsoft.EntityFrameworkCore": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "vNe+y8ZsEf1CsfmfYttfKAz/IgCCtphgguvao0HWNJNdjZf9cabD288nZJ17b/WaQMWXhLwYAsofk8vNVkfTOA==", + "resolved": "6.0.25", + "contentHash": "txcqw2xrmvMoTIgzAdUk8JHLELofGgTK3i6glswVZs4SC8BOU1M/iSAtwMIVtAtfzxuBIUAbHPx+Ly6lfkYe7g==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "6.0.22", - "Microsoft.EntityFrameworkCore.Analyzers": "6.0.22", + "Microsoft.EntityFrameworkCore.Abstractions": "6.0.25", + "Microsoft.EntityFrameworkCore.Analyzers": "6.0.25", "Microsoft.Extensions.Caching.Memory": "6.0.1", "Microsoft.Extensions.DependencyInjection": "6.0.1", "Microsoft.Extensions.Logging": "6.0.0", @@ -124,39 +143,48 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "82SZfdrLe7bdDB8/3INV0UULvlUzsdHkrEYylDCrzFXRWHXG9eO5jJQjRHU8j9XkGIN+MSPgIlczBnqeDvB36A==" + "resolved": "6.0.25", + "contentHash": "i6UpdWqWxSBbIFOkaMoubM40yIjTZO+0rIUkY5JRltSeFI4PzncBBQcNVNXXjAmiLXF/xY0xTS+ykClbkV46Yg==" + }, + "Microsoft.EntityFrameworkCore.Design": { + "type": "Transitive", + "resolved": "6.0.25", + "contentHash": "YawyMKj1f+GkwHrxMIf9tX84sMGgLFa5YoRmyuUugGhffiubkVLYIrlm4W0uSy2NzX4t6+V7keFLQf7lRQvDmA==", + "dependencies": { + "Humanizer.Core": "2.8.26", + "Microsoft.EntityFrameworkCore.Relational": "6.0.25" + } }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "W7yfdEbEuS1OPPxU0EOA6haqI4uvzs7OwHKh81DiJFn3NFNP2ztSovkOzBDhTwHX0j+OySsAj3BEJhuzTVYIVw==", + "resolved": "6.0.25", + "contentHash": "ci2lR++x7R7LR71+HoeRnB9Z5VeOQ1ILLbFRhsjjWZyLrAMkdq7TK9Ll47jo1TXDWF8Ddeap1JgcptgPKkWSRA==", "dependencies": { - "Microsoft.EntityFrameworkCore": "6.0.22", + "Microsoft.EntityFrameworkCore": "6.0.25", "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" } }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "EDKnYZtxq7P131xxLsEokda86WnFRiVAveLVAYR8kzyWl/UwTpf/RS2m2FrbH/U8vX3A+IQNpabtxcjtCUrY0g==", + "resolved": "6.0.25", + "contentHash": "vaQNuXgUN0nIzFXQiPSb9iAaJqLvZA164Sx9mjF5rFQS5cwQ/AiymF0e4J0QH3P07Mf3zEVZE5u2fTO0NacuMQ==", "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.22", + "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.25", "SQLitePCLRaw.bundle_e_sqlite3": "2.1.2" } }, "Microsoft.EntityFrameworkCore.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "xSU77ORQgwlD+s5Cmlk9DzoSCu5oxlHLuQl+v5zAZ0Uv5yH17hp02TBfz3x9nBA+CrIsqaLjGEuyZmLDf/5ATw==", + "resolved": "6.0.25", + "contentHash": "IU4E8I9FS2sUVxJJ0w/4jogLQ8C0zvu/SO6b1tRmiiCtTrHhjUB0tqhxjrFnDXZ/mpCJOElw50+qhbcElm0CYw==", "dependencies": { - "Microsoft.Data.Sqlite.Core": "6.0.22", - "Microsoft.EntityFrameworkCore.Relational": "6.0.22", + "Microsoft.Data.Sqlite.Core": "6.0.25", + "Microsoft.EntityFrameworkCore.Relational": "6.0.25", "Microsoft.Extensions.DependencyModel": "6.0.0" } }, @@ -257,10 +285,10 @@ }, "Microsoft.Extensions.Diagnostics.HealthChecks": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "HB1Zp1NY9m+HwYKLZBgUfNIt0xXzm4APARDuAIPODl8pT4g10oOiEDN8asOzx/sfL9xM+Sse5Zne9L+6qYi/iA==", + "resolved": "6.0.25", + "contentHash": "9vz47iGkzqhh0bGqomOTxaJNEEajeNcbSTSWwhh9Soo9lWm0UdPbw04CxXCQJPhc0aw9OaMnOxx7sCcde8/adA==", "dependencies": { - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.22", + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.25", "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", "Microsoft.Extensions.Logging.Abstractions": "6.0.4", "Microsoft.Extensions.Options": "6.0.0" @@ -268,8 +296,8 @@ }, "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "yvz+0r3qAt6gNEKlGSBO1BXMhtD3Tt8yzU59dHASolpwlSHvgqy0tEP6KXn3MPoKlPr0CiAHUdzOwYSoljzRSg==" + "resolved": "6.0.25", + "contentHash": "9sd1K/rp/vlxrBWNa0i8fgHCBPg94cocGMsJr7z9e2zQGQxMHNGpspdcy/FRGPAh2CINQet/RrM6Ef196xI20w==" }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", @@ -354,8 +382,8 @@ }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -365,10 +393,10 @@ }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } @@ -586,11 +614,12 @@ "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { + "HL7-dotnetcore": "[2.36.0, )", "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.22, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.25, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[1.0.4, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging": "[1.0.5, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Storage": "[0.2.18, )", "fo-dicom": "[5.1.1, )" } @@ -620,8 +649,8 @@ "monai.deploy.informaticsgateway.database.entityframework": { "type": "Project", "dependencies": { - "Microsoft.EntityFrameworkCore": "[6.0.22, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.22, )", + "Microsoft.EntityFrameworkCore": "[6.0.25, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.25, )", "Microsoft.Extensions.Configuration.FileExtensions": "[6.0.0, )", "Microsoft.Extensions.Configuration.Json": "[6.0.0, )", "Monai.Deploy.InformaticsGateway.Api": "[0.4.1, )", diff --git a/src/InformaticsGateway/Common/DestinationNotSuppliedException.cs b/src/InformaticsGateway/Common/DestinationNotSuppliedException.cs new file mode 100755 index 000000000..fcae49e38 --- /dev/null +++ b/src/InformaticsGateway/Common/DestinationNotSuppliedException.cs @@ -0,0 +1,43 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * Copyright 2019-2020 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.Runtime.Serialization; + +namespace Monai.Deploy.InformaticsGateway.Common +{ + + public class DestinationNotSuppliedException : Exception + { + public DestinationNotSuppliedException() + { + } + + public DestinationNotSuppliedException(string message) : base(message) + { + } + + public DestinationNotSuppliedException(string message, Exception innerException) : base(message, innerException) + { + } + + protected DestinationNotSuppliedException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + } +} diff --git a/src/InformaticsGateway/InternalVisibleTo.cs b/src/InformaticsGateway/InternalVisibleTo.cs old mode 100644 new mode 100755 diff --git a/src/InformaticsGateway/Logging/Log.100.200.ScpService.cs b/src/InformaticsGateway/Logging/Log.100.200.ScpService.cs index df26a9cf3..d3c420f1f 100755 --- a/src/InformaticsGateway/Logging/Log.100.200.ScpService.cs +++ b/src/InformaticsGateway/Logging/Log.100.200.ScpService.cs @@ -62,14 +62,12 @@ public static partial class Log public static partial void FailedToUpdateAppliationEntityHandlerWithUpdatedAEChange(this ILogger logger, string aeTitle, Exception? ex = null); // SCP Service - [LoggerMessage(EventId = 200, Level = LogLevel.Information, Message = "Initializing SCP Service at port {port}...")] - public static partial void ScpServiceLoading(this ILogger logger, int port); [LoggerMessage(EventId = 201, Level = LogLevel.Critical, Message = "Failed to initialize SCP listener.")] public static partial void ScpListenerInitializationFailure(this ILogger logger); - [LoggerMessage(EventId = 202, Level = LogLevel.Information, Message = "SCP listening on port: {port}.")] - public static partial void ScpListeningOnPort(this ILogger logger, int port); + [LoggerMessage(EventId = 202, Level = LogLevel.Information, Message = "{serviceName} listening on port: {port}.")] + public static partial void ScpListeningOnPort(this ILogger logger, string serviceName, int port); [LoggerMessage(EventId = 203, Level = LogLevel.Information, Message = "C-ECHO request received.")] public static partial void CEchoReceived(this ILogger logger); @@ -106,5 +104,8 @@ public static partial class Log [LoggerMessage(EventId = 214, Level = LogLevel.Information, Message = "Connection closed. Correlation ID={correlationId}. Calling AE Title={callingAeTitle}. Called AE Title={calledAeTitle}. Duration={durationSeconds} seconds.")] public static partial void ConnectionClosed(this ILogger logger, string correlationId, string callingAeTitle, string calledAeTitle, double durationSeconds); + + [LoggerMessage(EventId = 215, Level = LogLevel.Warning, Message = "Failed to find stored external app details for studyInstance Uid {studyInstanceUid}.")] + public static partial void FailedToFindStoredExtAppDetails(this ILogger logger, string studyInstanceUid); } } diff --git a/src/InformaticsGateway/Logging/Log.4000.ObjectUploadService.cs b/src/InformaticsGateway/Logging/Log.4000.ObjectUploadService.cs old mode 100644 new mode 100755 index afdad8c78..8939da80e --- a/src/InformaticsGateway/Logging/Log.4000.ObjectUploadService.cs +++ b/src/InformaticsGateway/Logging/Log.4000.ObjectUploadService.cs @@ -21,8 +21,8 @@ namespace Monai.Deploy.InformaticsGateway.Logging { public static partial class Log { - [LoggerMessage(EventId = 4000, Level = LogLevel.Warning, Message = "Failed to upload file {identifier}; added back to queue for retry.")] - public static partial void FailedToUploadFile(this ILogger logger, string identifier, Exception ex); + [LoggerMessage(EventId = 4000, Level = LogLevel.Warning, Message = "Failed to upload file {identifier}; path: {path} added back to queue for retry.")] + public static partial void FailedToUploadFile(this ILogger logger, string identifier, string path, Exception ex); [LoggerMessage(EventId = 4001, Level = LogLevel.Debug, Message = "Upload statistics: {threads} threads, {seconds} seconds.")] public static partial void UploadStats(this ILogger logger, int threads, double seconds); diff --git a/src/InformaticsGateway/Logging/Log.500.ExportService.cs b/src/InformaticsGateway/Logging/Log.500.ExportService.cs index 1588b94eb..3fb1ff065 100755 --- a/src/InformaticsGateway/Logging/Log.500.ExportService.cs +++ b/src/InformaticsGateway/Logging/Log.500.ExportService.cs @@ -135,5 +135,8 @@ public static partial class Log [LoggerMessage(EventId = 537, Level = LogLevel.Error, Message = "Error executing OutputDataEngine plug-in.")] public static partial void OutputDataEngineBlockException(this ILogger logger, Exception ex); + + [LoggerMessage(EventId = 538, Level = LogLevel.Error, Message = "Error exporting to Hl7 destination. Waiting {timeSpan} before next retry. Retry attempt {retryCount}.")] + public static partial void HL7ExportErrorWithRetry(this ILogger logger, TimeSpan timespan, int retryCount, Exception ex); } } diff --git a/src/InformaticsGateway/Logging/Log.5000.DataPlugins.cs b/src/InformaticsGateway/Logging/Log.5000.DataPlugins.cs old mode 100644 new mode 100755 index 2e2425b63..0dbfd93ff --- a/src/InformaticsGateway/Logging/Log.5000.DataPlugins.cs +++ b/src/InformaticsGateway/Logging/Log.5000.DataPlugins.cs @@ -14,6 +14,7 @@ * limitations under the License. */ +using System; using Microsoft.Extensions.Logging; namespace Monai.Deploy.InformaticsGateway.Logging @@ -37,5 +38,11 @@ public static partial class Log [LoggerMessage(EventId = 5005, Level = LogLevel.Information, Message = "Executing output data plug-in: {plugin}.")] public static partial void ExecutingOutputDataPlugIn(this ILogger logger, string plugin); + + [LoggerMessage(EventId = 5006, Level = LogLevel.Debug, Message = "Adding SCP Listener {serviceName} on port {port}")] + public static partial void AddingScpListener(this ILogger logger, string serviceName, int port); + + [LoggerMessage(EventId = 5007, Level = LogLevel.Error, Message = "Error executing plug-in: {plugin}.")] + public static partial void ErrorAddingOutputDataPlugIn(this ILogger logger, Exception d, string plugin); } } diff --git a/src/InformaticsGateway/Logging/Log.700.PayloadService.cs b/src/InformaticsGateway/Logging/Log.700.PayloadService.cs index 7720e70f4..0b926a413 100755 --- a/src/InformaticsGateway/Logging/Log.700.PayloadService.cs +++ b/src/InformaticsGateway/Logging/Log.700.PayloadService.cs @@ -158,7 +158,10 @@ public static partial class Log [LoggerMessage(EventId = 750, Level = LogLevel.Information, Message = "Artifact recieved published to {queue}, message ID={messageId}. Payload took {durationSeconds} seconds to complete.")] public static partial void ArtifactRecievedPublished(this ILogger logger, string queue, string messageId, double durationSeconds); - [LoggerMessage(EventId = 751, Level = LogLevel.Debug, Message = "NotifyAsync for payload {payloadId}.")] - public static partial void PayloadNotifyAsync(this ILogger logger, Guid payloadId); + [LoggerMessage(EventId = 751, Level = LogLevel.Debug, Message = "Saving External App Data to repo for {studyInstanceUID}.")] + public static partial void SavingExternalAppData(this ILogger logger, string studyInstanceUID); + + [LoggerMessage(EventId = 752, Level = LogLevel.Debug, Message = "Payload in Notification handler {payloadId}.")] + public static partial void PayloadNotifyAsync(this ILogger logger, string payloadId); } } diff --git a/src/InformaticsGateway/Logging/Log.800.Hl7Service.cs b/src/InformaticsGateway/Logging/Log.800.Hl7Service.cs old mode 100644 new mode 100755 index 419060787..15694a8f5 --- a/src/InformaticsGateway/Logging/Log.800.Hl7Service.cs +++ b/src/InformaticsGateway/Logging/Log.800.Hl7Service.cs @@ -68,5 +68,42 @@ public static partial class Log [LoggerMessage(EventId = 815, Level = LogLevel.Information, Message = "HL7 client {clientId} disconnected.")] public static partial void Hl7ClientRemoved(this ILogger logger, Guid clientId); + + [LoggerMessage(EventId = 816, Level = LogLevel.Debug, Message = "HL7 config loaded. {config}")] + public static partial void Hl7ConfigLoaded(this ILogger logger, string config); + + [LoggerMessage(EventId = 817, Level = LogLevel.Information, Message = "No HL7 config found")] + public static partial void Hl7NoConfig(this ILogger logger); + + [LoggerMessage(EventId = 818, Level = LogLevel.Debug, Message = "HL7 no matching config found for message {message}")] + public static partial void Hl7NoMatchingConfig(this ILogger logger, string message); + + [LoggerMessage(EventId = 819, Level = LogLevel.Debug, Message = "HL7 found matching config found for. {Id} config: {config}")] + public static partial void Hl7FoundMatchingConfig(this ILogger logger, string Id, string config); + + [LoggerMessage(EventId = 820, Level = LogLevel.Warning, Message = "HL7 exception thrown extracting Hl7 Info")] + public static partial void Hl7ExceptionThrow(this ILogger logger, Exception ex); + + [LoggerMessage(EventId = 821, Level = LogLevel.Warning, Message = "HL7 external App Details not found")] + public static partial void Hl7ExtAppDetailsNotFound(this ILogger logger); + + [LoggerMessage(EventId = 822, Level = LogLevel.Debug, Message = "HL7 changing value {hl7Tag} from {oldValue} to {newValue}")] + public static partial void ChangingHl7Values(this ILogger logger, string hl7Tag, string oldValue, string newValue); + + [LoggerMessage(EventId = 823, Level = LogLevel.Error, Message = "HL7 destination stream not writable")] + public static partial void Hl7ClientStreamNotWritable(this ILogger logger); + + [LoggerMessage(EventId = 824, Level = LogLevel.Error, Message = "HL7 Ack missing start or end characters")] + public static partial void Hl7AckMissingStartOrEndCharacters(this ILogger logger); + + [LoggerMessage(EventId = 825, Level = LogLevel.Error, Message = "HL7 Execption sending Hl7 meassage")] + public static partial void Hl7SendException(this ILogger logger, Exception ex); + + [LoggerMessage(EventId = 826, Level = LogLevel.Debug, Message = "HL7 meassage sent received {ack}")] + public static partial void Hl7MessageSent(this ILogger logger, string ack); + + [LoggerMessage(EventId = 827, Level = LogLevel.Warning, Message = "HL7 plugin loading exceptions")] + public static partial void HL7PluginLoadingExceptions(this ILogger logger, Exception ex); + } } diff --git a/src/InformaticsGateway/Logging/Log.8000.HttpServices.cs b/src/InformaticsGateway/Logging/Log.8000.HttpServices.cs index 48fcdd325..ec6d41ce8 100644 --- a/src/InformaticsGateway/Logging/Log.8000.HttpServices.cs +++ b/src/InformaticsGateway/Logging/Log.8000.HttpServices.cs @@ -179,5 +179,34 @@ public static partial class Log // [LoggerMessage(EventId = 8300, Level = LogLevel.Error, Message = "Unexpected error occurred in GET /dicom-associations API..")] public static partial void DicomAssociationsControllerGetError(this ILogger logger, Exception ex); + + /// + /// HL7 Application Configuration controller + /// + [LoggerMessage(EventId = 8400, Level = LogLevel.Error, Message = "Unexpected error occurred in PUT {endpoint} API.")] + public static partial void PutHl7ApplicationConfigException(this ILogger logger, string endpoint, Exception ex); + + + // HL7 Destination Controller + [LoggerMessage(EventId = 8401, Level = LogLevel.Information, Message = "HL7 destination added AE Title={aeTitle}, Host/IP={hostIp}.")] + public static partial void HL7DestinationEntityAdded(this ILogger logger, string aeTitle, string hostIp); + + [LoggerMessage(EventId = 8402, Level = LogLevel.Information, Message = "HL7 destination deleted {name}.")] + public static partial void HL7DestinationEntityDeleted(this ILogger logger, string name); + + [LoggerMessage(EventId = 8403, Level = LogLevel.Error, Message = "Error querying HL7 destinations.")] + public static partial void ErrorListingHL7DestinationEntities(this ILogger logger, Exception ex); + + [LoggerMessage(EventId = 8404, Level = LogLevel.Error, Message = "Error adding new HL7 destination.")] + public static partial void ErrorAddingHL7DestinationEntity(this ILogger logger, Exception ex); + + [LoggerMessage(EventId = 8405, Level = LogLevel.Error, Message = "Error deleting HL7 destination.")] + public static partial void ErrorDeletingHL7DestinationEntity(this ILogger logger, Exception ex); + + [LoggerMessage(EventId = 8406, Level = LogLevel.Error, Message = "Error C-ECHO to HL7 destination {name}.")] + public static partial void ErrorCEechoHL7DestinationEntity(this ILogger logger, string name, Exception ex); + + [LoggerMessage(EventId = 8407, Level = LogLevel.Information, Message = "HL7 destination updated {name}: AE Title={aeTitle}, Host/IP={hostIp}, Port={port}.")] + public static partial void HL7DestinationEntityUpdated(this ILogger logger, string name, string aeTitle, string hostIp, int port); } } diff --git a/src/InformaticsGateway/Monai.Deploy.InformaticsGateway.csproj b/src/InformaticsGateway/Monai.Deploy.InformaticsGateway.csproj index a57bcabc0..f1f927dd4 100755 --- a/src/InformaticsGateway/Monai.Deploy.InformaticsGateway.csproj +++ b/src/InformaticsGateway/Monai.Deploy.InformaticsGateway.csproj @@ -38,7 +38,11 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/src/InformaticsGateway/Program.cs b/src/InformaticsGateway/Program.cs index aaf5d9d91..be43e4ade 100755 --- a/src/InformaticsGateway/Program.cs +++ b/src/InformaticsGateway/Program.cs @@ -25,6 +25,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api.Mllp; using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; @@ -35,7 +36,6 @@ using Monai.Deploy.InformaticsGateway.Services.DicomWeb; using Monai.Deploy.InformaticsGateway.Services.Export; using Monai.Deploy.InformaticsGateway.Services.Fhir; -using Monai.Deploy.InformaticsGateway.Services.HealthLevel7; using Monai.Deploy.InformaticsGateway.Services.Http; using Monai.Deploy.InformaticsGateway.Services.Scp; using Monai.Deploy.InformaticsGateway.Services.Scu; @@ -110,13 +110,16 @@ internal static IHostBuilder CreateHostBuilder(string[] args) => services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped, InputDataPlugInEngineFactory>(); services.AddScoped, OutputDataPlugInEngineFactory>(); + services.AddScoped, InputHL7DataPlugInEngineFactory>(); services.AddMonaiDeployStorageService(hostContext.Configuration!.GetSection("InformaticsGateway:storage:serviceAssemblyName").Value, Monai.Deploy.Storage.HealthCheckOptions.ServiceHealthCheck); @@ -132,16 +135,9 @@ internal static IHostBuilder CreateHostBuilder(string[] args) => services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + var timeout = TimeSpan.FromSeconds(hostContext.Configuration.GetValue("InformaticsGateway:dicomWeb:clientTimeout", DicomWebConfiguration.DefaultClientTimeout)); services @@ -158,16 +154,17 @@ internal static IHostBuilder CreateHostBuilder(string[] args) => .AddHttpClient("fhir", configure => configure.Timeout = timeout) .SetHandlerLifetime(timeout); -#pragma warning disable CS8603 // Possible null reference return. - services.AddHostedService(p => p.GetService()); - services.AddHostedService(p => p.GetService()); - services.AddHostedService(p => p.GetService()); - services.AddHostedService(p => p.GetService()); - services.AddHostedService(p => p.GetService()); - services.AddHostedService(p => p.GetService()); - services.AddHostedService(p => p.GetService()); - services.AddHostedService(p => p.GetService()); -#pragma warning restore CS8603 // Possible null reference return. + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); }) .ConfigureWebHostDefaults(webBuilder => diff --git a/src/InformaticsGateway/Properties/launchSettings.json b/src/InformaticsGateway/Properties/launchSettings.json deleted file mode 100755 index f5636e031..000000000 --- a/src/InformaticsGateway/Properties/launchSettings.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "profiles": { - "Monai.Deploy.InformaticsGateway": { - "commandName": "Project", - "environmentVariables": { - "DOTNET_ENVIRONMENT": "Development", - "ELASTIC_CLIENT_APIVERSIONING": "true", - "LOGSTASH_URL": "tcp://localhost:50000", - "InformaticsGateway__messaging__publisherSettings__username": "rabbitmq", - "InformaticsGateway__messaging__publisherSettings__password": "rabbitmq", - "InformaticsGateway__messaging__subscriberSettings__username": "rabbitmq", - "InformaticsGateway__messaging__subscriberSettings__password": "rabbitmq", - "Kestrel__EndPoints__Http__Url": "http://+:5000" - } - } - } -} \ No newline at end of file diff --git a/src/InformaticsGateway/Services/Common/IInputDataPluginEngineFactory.cs b/src/InformaticsGateway/Services/Common/IInputDataPluginEngineFactory.cs old mode 100644 new mode 100755 index 4f122da6f..880eaef95 --- a/src/InformaticsGateway/Services/Common/IInputDataPluginEngineFactory.cs +++ b/src/InformaticsGateway/Services/Common/IInputDataPluginEngineFactory.cs @@ -125,4 +125,11 @@ public OutputDataPlugInEngineFactory(IFileSystem fileSystem, ILogger + { + public InputHL7DataPlugInEngineFactory(IFileSystem fileSystem, ILogger> logger) : base(fileSystem, logger) + { + } + } } diff --git a/src/InformaticsGateway/Services/Common/InputHL7DataPlugInEngine.cs b/src/InformaticsGateway/Services/Common/InputHL7DataPlugInEngine.cs new file mode 100755 index 000000000..bd8fdea29 --- /dev/null +++ b/src/InformaticsGateway/Services/Common/InputHL7DataPlugInEngine.cs @@ -0,0 +1,93 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using HL7.Dotnetcore; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; +using Monai.Deploy.InformaticsGateway.Api.Storage; +using Monai.Deploy.InformaticsGateway.Common; +using Monai.Deploy.InformaticsGateway.Logging; + +namespace Monai.Deploy.InformaticsGateway.Services.Common +{ + public class InputHL7DataPlugInEngine : IInputHL7DataPlugInEngine + { + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private IReadOnlyList? _plugsins; + + public InputHL7DataPlugInEngine(IServiceProvider serviceProvider, ILogger logger) + { + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public void Configure(IReadOnlyList pluginAssemblies) + { + _plugsins = LoadPlugIns(_serviceProvider, pluginAssemblies); + } + + public async Task> ExecutePlugInsAsync(Message hl7File, FileStorageMetadata fileMetadata, Hl7ApplicationConfigEntity? configItem) + { + if (_plugsins == null) + { + throw new PlugInInitializationException("InputHL7DataPlugInEngine not configured, please call Configure() first."); + } + + foreach (var plugin in _plugsins) + { + var nm = plugin.ToString(); + if (configItem is not null && configItem.PlugInAssemblies.Any(a => a.StartsWith(plugin.ToString()!))) + { + _logger.ExecutingInputDataPlugIn(plugin.Name); + (hl7File, fileMetadata) = await plugin.ExecuteAsync(hl7File, fileMetadata).ConfigureAwait(false); + } + } + + return new Tuple(hl7File, fileMetadata); + } + + private IReadOnlyList LoadPlugIns(IServiceProvider serviceProvider, IReadOnlyList pluginAssemblies) + { + var exceptions = new List(); + var list = new List(); + foreach (var plugin in pluginAssemblies) + { + try + { + _logger.AddingInputDataPlugIn(plugin); + list.Add(typeof(IInputHL7DataPlugIn).CreateInstance(serviceProvider, typeString: plugin)); + } + catch (Exception ex) + { + exceptions.Add(new PlugInLoadingException($"Error loading plug-in '{plugin}'.", ex)); + } + } + + if (exceptions.Any()) + { + throw new AggregateException("Error loading plug-in(s).", exceptions); + } + + return list; + } + } +} diff --git a/src/InformaticsGateway/Services/Common/OutputDataPluginEngine.cs b/src/InformaticsGateway/Services/Common/OutputDataPluginEngine.cs index 33d72b186..a497862dc 100755 --- a/src/InformaticsGateway/Services/Common/OutputDataPluginEngine.cs +++ b/src/InformaticsGateway/Services/Common/OutputDataPluginEngine.cs @@ -20,7 +20,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Logging; @@ -61,7 +61,7 @@ public async Task ExecutePlugInsAsync(ExportRequestDat (dicomFile, exportRequestDataMessage) = await plugin.ExecuteAsync(dicomFile, exportRequestDataMessage).ConfigureAwait(false); } using var ms = new MemoryStream(); - await dicomFile.SaveAsync(ms); + await dicomFile.SaveAsync(ms).ConfigureAwait(false); exportRequestDataMessage.SetData(ms.ToArray()); return exportRequestDataMessage; @@ -80,6 +80,7 @@ private IReadOnlyList LoadPlugIns(IServiceProvider servicePro } catch (Exception ex) { + _logger.ErrorAddingOutputDataPlugIn(ex, plugin); exceptions.Add(new PlugInLoadingException($"Error loading plug-in '{plugin}'.", ex)); } } diff --git a/src/InformaticsGateway/Services/Common/ScpInputTypeEnum.cs b/src/InformaticsGateway/Services/Common/ScpInputTypeEnum.cs new file mode 100755 index 000000000..280effdfe --- /dev/null +++ b/src/InformaticsGateway/Services/Common/ScpInputTypeEnum.cs @@ -0,0 +1,24 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 Monai.Deploy.InformaticsGateway.Services.Common +{ + public enum ScpInputTypeEnum + { + WorkflowTrigger, + ExternalAppReturn + } +} diff --git a/src/InformaticsGateway/Services/Connectors/DataRetrievalService.cs b/src/InformaticsGateway/Services/Connectors/DataRetrievalService.cs old mode 100644 new mode 100755 index 3bc1300fd..b558c4175 --- a/src/InformaticsGateway/Services/Connectors/DataRetrievalService.cs +++ b/src/InformaticsGateway/Services/Connectors/DataRetrievalService.cs @@ -29,7 +29,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; using Monai.Deploy.InformaticsGateway.Api.Rest; using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Common; @@ -124,7 +123,7 @@ private async Task BackgroundProcessing(CancellationToken cancellationToken) try { request = await repository.TakeAsync(cancellationToken).ConfigureAwait(false); - using (_logger.BeginScope(new LoggingDataDictionary { { "TransactionId", request.TransactionId } })) + using (_logger.BeginScope(new Api.LoggingDataDictionary { { "TransactionId", request.TransactionId } })) { _logger.ProcessingInferenceRequest(); await ProcessRequest(request, cancellationToken).ConfigureAwait(false); @@ -589,4 +588,4 @@ public void Dispose() #endregion Data Retrieval } -} \ No newline at end of file +} diff --git a/src/InformaticsGateway/Services/Connectors/PayloadAssembler.cs b/src/InformaticsGateway/Services/Connectors/PayloadAssembler.cs index d09a98b8d..1222add24 100755 --- a/src/InformaticsGateway/Services/Connectors/PayloadAssembler.cs +++ b/src/InformaticsGateway/Services/Connectors/PayloadAssembler.cs @@ -200,13 +200,13 @@ private async Task QueueBucketForNotification(string key, Payload payload) } } - private async Task CreateOrGetPayload(string key, string correlationId, string? workflowInstanceId, string? taskId, Messaging.Events.DataOrigin dataOrigin, uint timeout, string? payloadId = null) + private async Task CreateOrGetPayload(string key, string correlationId, string? workflowInstanceId, string? taskId, Messaging.Events.DataOrigin dataOrigin, uint timeout, string? destinationFolder = null) { return await _payloads.GetOrAdd(key, x => new AsyncLazy(async () => { var scope = _serviceScopeFactory.CreateScope(); var repository = scope.ServiceProvider.GetRequiredService(); - var newPayload = new Payload(key, correlationId, workflowInstanceId, taskId, dataOrigin, timeout, payloadId); + var newPayload = new Payload(key, correlationId, workflowInstanceId, taskId, dataOrigin, timeout, null, destinationFolder); await repository.AddAsync(newPayload).ConfigureAwait(false); _logger.BucketCreated(key, timeout); return newPayload; diff --git a/src/InformaticsGateway/Services/Connectors/PayloadNotificationActionHandler.cs b/src/InformaticsGateway/Services/Connectors/PayloadNotificationActionHandler.cs index 5dd3ebf81..1aa2a848a 100755 --- a/src/InformaticsGateway/Services/Connectors/PayloadNotificationActionHandler.cs +++ b/src/InformaticsGateway/Services/Connectors/PayloadNotificationActionHandler.cs @@ -62,7 +62,7 @@ public PayloadNotificationActionHandler(IServiceScopeFactory serviceScopeFactory public async Task NotifyAsync(Payload payload, ActionBlock notificationQueue, CancellationToken cancellationToken = default) { - _logger.PayloadNotifyAsync(payload.PayloadId); + _logger.PayloadNotifyAsync(payload.PayloadId.ToString()); Guard.Against.Null(payload, nameof(payload)); Guard.Against.Null(notificationQueue, nameof(notificationQueue)); diff --git a/src/InformaticsGateway/Services/DicomWeb/ContentTypes.cs b/src/InformaticsGateway/Services/DicomWeb/ContentTypes.cs index 7bb731f23..4fc30edea 100644 --- a/src/InformaticsGateway/Services/DicomWeb/ContentTypes.cs +++ b/src/InformaticsGateway/Services/DicomWeb/ContentTypes.cs @@ -22,6 +22,7 @@ internal static class ContentTypes public const string ApplicationDicomJson = "application/dicom+json"; public const string ApplicationDicomXml = "application/dicom+xml"; public const string ApplicationOctetStream = "application/octet-stream"; + public const string ApplicationJson = "application/json"; public const string MultipartRelated = "multipart/related"; diff --git a/src/InformaticsGateway/Services/Export/DicomWebExportService.cs b/src/InformaticsGateway/Services/Export/DicomWebExportService.cs index a2baf45a5..9eca29da2 100755 --- a/src/InformaticsGateway/Services/Export/DicomWebExportService.cs +++ b/src/InformaticsGateway/Services/Export/DicomWebExportService.cs @@ -26,7 +26,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Api.Rest; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; @@ -35,6 +35,7 @@ using Monai.Deploy.InformaticsGateway.DicomWeb.Client.API; using Monai.Deploy.InformaticsGateway.Logging; using Monai.Deploy.InformaticsGateway.Services.Common; +using Monai.Deploy.Messaging.Common; using Monai.Deploy.Messaging.Events; using Polly; @@ -44,7 +45,6 @@ internal class DicomWebExportService : ExportServiceBase { private readonly ILoggerFactory _loggerFactory; private readonly IHttpClientFactory _httpClientFactory; - private readonly IServiceScopeFactory _serviceScopeFactory; private readonly ILogger _logger; private readonly IOptions _configuration; private readonly IDicomToolkit _dicomToolkit; @@ -60,11 +60,10 @@ public DicomWebExportService( ILogger logger, IOptions configuration, IDicomToolkit dicomToolkit) - : base(logger, configuration, serviceScopeFactory) + : base(logger, configuration, serviceScopeFactory, dicomToolkit) { _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); - _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _dicomToolkit = dicomToolkit ?? throw new ArgumentNullException(nameof(dicomToolkit)); @@ -73,11 +72,15 @@ public DicomWebExportService( Concurrency = configuration.Value.DicomWeb.MaximumNumberOfConnection; } + protected override async Task ProcessMessage(MessageReceivedEventArgs eventArgs) + { + await BaseProcessMessage(eventArgs); + } protected override async Task ExportDataBlockCallback(ExportRequestDataMessage exportRequestData, CancellationToken cancellationToken) { - using var loggerScope = _logger.BeginScope(new LoggingDataDictionary { { "ExportTaskId", exportRequestData.ExportTaskId }, { "CorrelationId", exportRequestData.CorrelationId }, { "Filename", exportRequestData.Filename } }); + using var loggerScope = _logger.BeginScope(new Api.LoggingDataDictionary { { "ExportTaskId", exportRequestData.ExportTaskId }, { "CorrelationId", exportRequestData.CorrelationId }, { "Filename", exportRequestData.Filename } }); - using var scope = _serviceScopeFactory.CreateScope(); + using var scope = ServiceScopeFactory.CreateScope(); var repository = scope.ServiceProvider.GetRequiredService(); foreach (var transaction in exportRequestData.Destinations) @@ -174,7 +177,7 @@ private void CheckAndLogResult(DicomWebResponse result) break; default: - throw new ServiceException("Failed to export to destination."); + throw new InformaticsGateway.Common.ServiceException("Failed to export to destination."); } } } diff --git a/src/InformaticsGateway/Services/Export/ExportRequestEventDetails.cs b/src/InformaticsGateway/Services/Export/ExportRequestEventDetails.cs index ee3e4012c..66a704e63 100755 --- a/src/InformaticsGateway/Services/Export/ExportRequestEventDetails.cs +++ b/src/InformaticsGateway/Services/Export/ExportRequestEventDetails.cs @@ -29,7 +29,7 @@ public ExportRequestEventDetails(ExportRequestEvent exportRequest) ExportTaskId = exportRequest.ExportTaskId; Files = new List(exportRequest.Files); Destinations = new string[exportRequest.Destinations.Length]; - Array.Copy(exportRequest.Destinations, Destinations, exportRequest.Destinations.Length); + //Array.Copy(exportRequest.Destinations, Destinations, exportRequest.Destinations.Length); exportRequest.Destinations.CopyTo(Destinations, 0); DeliveryTag = exportRequest.DeliveryTag; MessageId = exportRequest.MessageId; @@ -42,6 +42,23 @@ public ExportRequestEventDetails(ExportRequestEvent exportRequest) StartTime = DateTimeOffset.UtcNow; } + public ExportRequestEventDetails(ExternalAppRequestEvent externalAppRequest) + { + CorrelationId = externalAppRequest.CorrelationId; + ExportTaskId = externalAppRequest.ExportTaskId; + Files = new List(externalAppRequest.Files); + Destinations = externalAppRequest.Targets.Select(t => t.Destination).ToArray(); + DeliveryTag = externalAppRequest.DeliveryTag; + MessageId = externalAppRequest.MessageId; + WorkflowInstanceId = externalAppRequest.WorkflowInstanceId; + PayloadId = externalAppRequest.DestinationFolder; + + PluginAssemblies.AddRange(externalAppRequest.PluginAssemblies); + ErrorMessages.AddRange(externalAppRequest.ErrorMessages); + + StartTime = DateTimeOffset.UtcNow; + } + /// /// Gets the time the export request received. /// diff --git a/src/InformaticsGateway/Services/Export/ExportServiceBase.cs b/src/InformaticsGateway/Services/Export/ExportServiceBase.cs index 4a2b2a0e3..0a80875f3 100755 --- a/src/InformaticsGateway/Services/Export/ExportServiceBase.cs +++ b/src/InformaticsGateway/Services/Export/ExportServiceBase.cs @@ -23,15 +23,18 @@ using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using Ardalis.GuardClauses; +using FellowOakDicom.Network; +using FellowOakDicom; +using FellowOakDicom.Network.Client; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Api.Rest; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; using Monai.Deploy.InformaticsGateway.Logging; using Monai.Deploy.InformaticsGateway.Services.Common; using Monai.Deploy.InformaticsGateway.Services.Storage; @@ -41,32 +44,37 @@ using Monai.Deploy.Messaging.Messages; using Monai.Deploy.Storage.API; using Polly; +using System.Net.Sockets; +using Monai.Deploy.InformaticsGateway.Api.Models; namespace Monai.Deploy.InformaticsGateway.Services.Export { public abstract class ExportServiceBase : IHostedService, IMonaiService, IDisposable { - private static readonly object SyncRoot = new(); + protected static readonly object SyncRoot = new(); internal event EventHandler? ReportActionCompleted; private readonly CancellationTokenSource _cancellationTokenSource; private readonly ILogger _logger; - private readonly IServiceScopeFactory _serviceScopeFactory; + protected readonly IServiceScopeFactory ServiceScopeFactory; private readonly InformaticsGatewayConfiguration _configuration; - private readonly IMessageBrokerSubscriberService _messageSubscriber; - private readonly IMessageBrokerPublisherService _messagePublisher; + protected readonly IMessageBrokerSubscriberService MessageSubscriber; + protected readonly IMessageBrokerPublisherService MessagePublisher; private readonly IServiceScope _scope; - private readonly Dictionary _exportRequests; + protected readonly Dictionary ExportRequests; private readonly IStorageInfoProvider _storageInfoProvider; private bool _disposedValue; private ulong _activeWorkers = 0; + private readonly IDicomToolkit _dicomToolkit; public abstract string RoutingKey { get; } protected abstract ushort Concurrency { get; } public ServiceStatus Status { get; set; } = ServiceStatus.Unknown; public abstract string ServiceName { get; } + protected string ExportCompleteTopic { get; set; } + /// /// Override the ExportDataBlockCallback method to customize export logic. /// Must update State to either Succeeded or Failed. @@ -76,15 +84,20 @@ public abstract class ExportServiceBase : IHostedService, IMonaiService, IDispos /// protected abstract Task ExportDataBlockCallback(ExportRequestDataMessage exportRequestData, CancellationToken cancellationToken); + protected abstract Task ProcessMessage(MessageReceivedEventArgs eventArgs); + protected ExportServiceBase( ILogger logger, IOptions configuration, - IServiceScopeFactory serviceScopeFactory) + IServiceScopeFactory serviceScopeFactory, + IDicomToolkit dicomToolkit) { _cancellationTokenSource = new CancellationTokenSource(); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); - _scope = _serviceScopeFactory.CreateScope(); + ServiceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); + _scope = ServiceScopeFactory.CreateScope(); + _dicomToolkit = dicomToolkit ?? throw new ArgumentNullException(nameof(dicomToolkit)); + if (configuration is null) { @@ -93,13 +106,14 @@ protected ExportServiceBase( _configuration = configuration.Value; - _messageSubscriber = _scope.ServiceProvider.GetRequiredService(); - _messagePublisher = _scope.ServiceProvider.GetRequiredService(); + ExportCompleteTopic = _configuration.Messaging.Topics.ExportComplete; + MessageSubscriber = _scope.ServiceProvider.GetRequiredService(); + MessagePublisher = _scope.ServiceProvider.GetRequiredService(); _storageInfoProvider = _scope.ServiceProvider.GetRequiredService(); - _exportRequests = new Dictionary(); + ExportRequests = new Dictionary(); - _messageSubscriber.OnConnectionError += (sender, args) => + MessageSubscriber.OnConnectionError += (sender, args) => { _logger.MessagingServiceErrorRecover(args.ErrorMessage); SetupPolling(); @@ -115,21 +129,25 @@ public Task StartAsync(CancellationToken cancellationToken) return Task.CompletedTask; } - public Task StopAsync(CancellationToken cancellationToken) + public async Task StopAsync(CancellationToken cancellationToken) { _cancellationTokenSource.Cancel(); _logger.ServiceStopping(ServiceName); Status = ServiceStatus.Stopped; - return Task.CompletedTask; +#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods + await Task.Delay(250).ConfigureAwait(false); +#pragma warning restore CA2016 // Forward the 'CancellationToken' parameter to methods + _cancellationTokenSource.Dispose(); + return; } private void SetupPolling() { - _messageSubscriber.SubscribeAsync(RoutingKey, RoutingKey, OnMessageReceivedCallback, prefetchCount: Concurrency); + MessageSubscriber.SubscribeAsync(RoutingKey, RoutingKey, OnMessageReceivedCallback, prefetchCount: Concurrency); _logger.ExportEventSubscription(ServiceName, RoutingKey); } - private async Task OnMessageReceivedCallback(MessageReceivedEventArgs eventArgs) + protected virtual async Task OnMessageReceivedCallback(MessageReceivedEventArgs eventArgs) { using var loggerScope = _logger.BeginScope(new Messaging.Common.LoggingDataDictionary { { "ThreadId", Environment.CurrentManagedThreadId }, @@ -138,7 +156,7 @@ private async Task OnMessageReceivedCallback(MessageReceivedEventArgs eventArgs) if (!_storageInfoProvider.HasSpaceAvailableForExport) { _logger.ExportServiceStoppedDueToLowStorageSpace(_storageInfoProvider.AvailableFreeSpace); - _messageSubscriber.Reject(eventArgs.Message); + MessageSubscriber.Reject(eventArgs.Message); return; } @@ -146,111 +164,134 @@ private async Task OnMessageReceivedCallback(MessageReceivedEventArgs eventArgs) { _logger.ExceededMaxmimumNumberOfWorkers(ServiceName, _activeWorkers); await Task.Delay(200).ConfigureAwait(false); // small delay to stop instantly dead lettering the next message. - _messageSubscriber.Reject(eventArgs.Message); + MessageSubscriber.Reject(eventArgs.Message); return; } Interlocked.Increment(ref _activeWorkers); try { - var executionOptions = new ExecutionDataflowBlockOptions + await ProcessMessage(eventArgs).ConfigureAwait(false); + } + catch (AggregateException ex) + { + foreach (var iex in ex.InnerExceptions) { - MaxDegreeOfParallelism = Concurrency, - MaxMessagesPerTask = 1, - CancellationToken = _cancellationTokenSource.Token - }; + _logger.ErrorExporting(iex); + } + } + catch (Exception ex) + { + _logger.ErrorProcessingExportTask(ex); + } + finally + { + Interlocked.Decrement(ref _activeWorkers); + } + } - var exportFlow = new TransformManyBlock( - exportRequest => DownloadPayloadActionCallback(exportRequest, _cancellationTokenSource.Token), - executionOptions); + TransformBlock GetoutputDataEngineBlock(ExecutionDataflowBlockOptions executionOptions) + { + return new TransformBlock( + async (exportDataRequest) => + { + try + { + if (exportDataRequest.IsFailed) return exportDataRequest; + return await ExecuteOutputDataEngineCallback(exportDataRequest).ConfigureAwait(false); + } + catch (Exception e) + { + exportDataRequest.SetFailed(FileExportStatus.ServiceError, $"failed to execute plugin {e.Message}"); + return exportDataRequest; + } + }, + executionOptions); + } - var outputDataEngineBLock = new TransformBlock( - async (exportDataRequest) => + TransformBlock GetxportActionBlock(ExecutionDataflowBlockOptions executionOptions) + { + return new TransformBlock( + async (exportDataRequest) => + { + try { - try - { - if (exportDataRequest.IsFailed) return exportDataRequest; - return await ExecuteOutputDataEngineCallback(exportDataRequest).ConfigureAwait(false); - } - catch (Exception e) - { - _logger.OutputDataEngineBlockException(e); - exportDataRequest.SetFailed(FileExportStatus.ServiceError, $"failed to execute plugin {e.Message}"); - return exportDataRequest; - } - }, - executionOptions); - - var exportActionBlock = new TransformBlock( - async (exportDataRequest) => + if (exportDataRequest.IsFailed) return exportDataRequest; + return await ExportDataBlockCallback(exportDataRequest, _cancellationTokenSource.Token).ConfigureAwait(false); + } + catch (Exception e) { - try - { - if (exportDataRequest.IsFailed) return exportDataRequest; - return await ExportDataBlockCallback(exportDataRequest, _cancellationTokenSource.Token).ConfigureAwait(false); - } - catch (Exception e) - { - exportDataRequest.SetFailed(FileExportStatus.ServiceError, $"Failed during export {e.Message}"); - return exportDataRequest; - } + exportDataRequest.SetFailed(FileExportStatus.ServiceError, $"Failed during export {e.Message}"); + return exportDataRequest; + } - }, - executionOptions); + }, + executionOptions); + } - var reportingActionBlock = new ActionBlock(ReportingActionBlock, executionOptions); + protected (TransformManyBlock, ActionBlock) SetupActionBlocks() + { + var executionOptions = new ExecutionDataflowBlockOptions + { + MaxDegreeOfParallelism = Concurrency, + MaxMessagesPerTask = 1, + CancellationToken = _cancellationTokenSource.Token + }; - var linkOptions = new DataflowLinkOptions { PropagateCompletion = true }; + var exportFlow = new TransformManyBlock( + exportRequest => DownloadPayloadActionCallback(exportRequest, _cancellationTokenSource.Token), + executionOptions); - exportFlow.LinkTo(outputDataEngineBLock, linkOptions); - outputDataEngineBLock.LinkTo(exportActionBlock, linkOptions); - exportActionBlock.LinkTo(reportingActionBlock, linkOptions); + var outputDataEngineBLock = GetoutputDataEngineBlock(executionOptions); - lock (SyncRoot) - { - var exportRequest = eventArgs.Message.ConvertTo(); - if (_exportRequests.ContainsKey(exportRequest.ExportTaskId)) - { - _logger.ExportRequestAlreadyQueued(exportRequest.CorrelationId, exportRequest.ExportTaskId); - return; - } + var exportActionBlock = GetxportActionBlock(executionOptions); - exportRequest.MessageId = eventArgs.Message.MessageId; - exportRequest.DeliveryTag = eventArgs.Message.DeliveryTag; + var reportingActionBlock = new ActionBlock(ReportingActionBlock, executionOptions); - var exportRequestWithDetails = new ExportRequestEventDetails(exportRequest); + var linkOptions = new DataflowLinkOptions { PropagateCompletion = true }; - _exportRequests.Add(exportRequest.ExportTaskId, exportRequestWithDetails); - if (!exportFlow.Post(exportRequestWithDetails)) - { - _logger.ErrorPostingExportJobToQueue(exportRequest.CorrelationId, exportRequest.ExportTaskId); - _messageSubscriber.Reject(eventArgs.Message); - } - else - { - _logger.ExportRequestQueuedForProcessing(exportRequest.CorrelationId, exportRequest.MessageId, exportRequest.ExportTaskId); - } - } + exportFlow.LinkTo(outputDataEngineBLock, linkOptions); + outputDataEngineBLock.LinkTo(exportActionBlock, linkOptions); + exportActionBlock.LinkTo(reportingActionBlock, linkOptions); - exportFlow.Complete(); - await reportingActionBlock.Completion.ConfigureAwait(false); - } - catch (AggregateException ex) - { - foreach (var iex in ex.InnerExceptions) - { - _logger.ErrorExporting(iex); - } - } - catch (Exception ex) + return (exportFlow, reportingActionBlock); + } + + protected void HandleCStoreException(Exception ex, ExportRequestDataMessage exportRequestData) + { + var exception = ex; + var fillStatus = FileExportStatus.ServiceError; + + if (exception is AggregateException) { - _logger.ErrorProcessingExportTask(ex); + exception = exception.InnerException!; } - finally + + var errorMessage = $"Job failed with error: {exception.Message}."; + + switch (exception) { - Interlocked.Decrement(ref _activeWorkers); + case DicomAssociationAbortedException abortEx: + errorMessage = $"Association aborted with reason {abortEx.AbortReason}."; + break; + case DicomAssociationRejectedException rejectEx: + errorMessage = $"Association rejected with reason {rejectEx.RejectReason}."; + break; + case SocketException socketException: + errorMessage = $"Association aborted with error {socketException.Message}."; + break; + case ConfigurationException configException: + errorMessage = $"{configException.Message}"; + fillStatus = FileExportStatus.ConfigurationError; + break; + case ExternalAppExeception appException: + errorMessage = $"{appException.Message}"; + break; } + + _logger.ExportException(errorMessage, ex); + exportRequestData.SetFailed(fillStatus, errorMessage); } // TPL doesn't yet support IAsyncEnumerable @@ -259,7 +300,7 @@ private IEnumerable DownloadPayloadActionCallback(Expo { Guard.Against.Null(exportRequest, nameof(exportRequest)); using var loggerScope = _logger.BeginScope(new Api.LoggingDataDictionary { { "ExportTaskId", exportRequest.ExportTaskId }, { "CorrelationId", exportRequest.CorrelationId } }); - var scope = _serviceScopeFactory.CreateScope(); + var scope = ServiceScopeFactory.CreateScope(); var storageService = scope.ServiceProvider.GetRequiredService(); foreach (var file in exportRequest.Files) @@ -282,6 +323,7 @@ private IEnumerable DownloadPayloadActionCallback(Expo var stream = (await storageService.GetObjectAsync(_configuration.Storage.StorageServiceBucketName, file, cancellationToken).ConfigureAwait(false) as MemoryStream)!; exportRequestData.SetData(stream.ToArray()); _logger.FileReadyForExport(file); + ExportCompleteCallback(exportRequestData).GetAwaiter().GetResult(); }); task.Wait(cancellationToken); @@ -297,7 +339,7 @@ private IEnumerable DownloadPayloadActionCallback(Expo } } - private async Task ExecuteOutputDataEngineCallback(ExportRequestDataMessage exportDataRequest) + protected virtual async Task ExecuteOutputDataEngineCallback(ExportRequestDataMessage exportDataRequest) { using var loggerScope = _logger.BeginScope(new Messaging.Common.LoggingDataDictionary { { "WorkflowInstanceId", exportDataRequest.WorkflowInstanceId }, @@ -309,9 +351,8 @@ private async Task ExecuteOutputDataEngineCallback(Exp return await outputDataEngine.ExecutePlugInsAsync(exportDataRequest).ConfigureAwait(false); } - private void ReportingActionBlock(ExportRequestDataMessage exportRequestData) + private static void HandleStatus(ExportRequestDataMessage exportRequestData, ExportRequestEventDetails exportRequest) { - var exportRequest = _exportRequests[exportRequestData.ExportTaskId]; lock (SyncRoot) { exportRequest.FileStatuses.Add(exportRequestData.Filename, exportRequestData.ExportStatus); @@ -328,11 +369,16 @@ private void ReportingActionBlock(ExportRequestDataMessage exportRequestData) { exportRequest.AddErrorMessages(exportRequestData.Messages); } + } + } - if (!exportRequest.IsCompleted) - { - return; - } + private void ReportingActionBlock(ExportRequestDataMessage exportRequestData) + { + var exportRequest = ExportRequests[exportRequestData.ExportTaskId]; + HandleStatus(exportRequestData, exportRequest); + if (!exportRequest.IsCompleted) + { + return; } using var loggerScope = _logger.BeginScope(new Api.LoggingDataDictionary { { "ExportTaskId", exportRequestData.ExportTaskId }, { "CorrelationId", exportRequestData.CorrelationId } }); @@ -342,6 +388,26 @@ private void ReportingActionBlock(ExportRequestDataMessage exportRequestData) var jsonMessage = new JsonMessage(exportCompleteEvent, MessageBrokerConfiguration.InformaticsGatewayApplicationId, exportRequest.CorrelationId, exportRequest.DeliveryTag); + FinaliseMessage(jsonMessage); + + lock (SyncRoot) + { + ExportRequests.Remove(exportRequestData.ExportTaskId); + } + + if (ReportActionCompleted != null) + { + _logger.CallingReportActionCompletedCallback(); + ReportActionCompleted(this, EventArgs.Empty); + } + } + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + protected virtual async Task ExportCompleteCallback(ExportRequestDataMessage exportRequestData) { } +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + + private void FinaliseMessage(JsonMessage jsonMessage) + { Policy .Handle() .WaitAndRetry( @@ -353,7 +419,7 @@ private void ReportingActionBlock(ExportRequestDataMessage exportRequestData) .Execute(() => { _logger.PublishingExportCompleteEvent(); - _messagePublisher.Publish(_configuration.Messaging.Topics.ExportComplete, jsonMessage.ToMessage()); + MessagePublisher.Publish(ExportCompleteTopic, jsonMessage.ToMessage()); }); Policy @@ -367,32 +433,188 @@ private void ReportingActionBlock(ExportRequestDataMessage exportRequestData) .Execute(() => { _logger.SendingAcknowledgement(); - _messageSubscriber.Acknowledge(jsonMessage); + MessageSubscriber.Acknowledge(jsonMessage); }); + } - lock (SyncRoot) + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - _exportRequests.Remove(exportRequestData.ExportTaskId); + if (disposing) + { + _scope.Dispose(); + } + + _disposedValue = true; } + } - if (ReportActionCompleted != null) + private async Task LookupDestinationAsync(string destinationName, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(destinationName)) { - _logger.CallingReportActionCompletedCallback(); - ReportActionCompleted(this, EventArgs.Empty); + throw new ConfigurationException("Export task does not have destination set."); } + + using var scope = ServiceScopeFactory.CreateScope(); + var repository = scope.ServiceProvider.GetRequiredService(); + var destination = await repository.FindByNameAsync(destinationName, cancellationToken).ConfigureAwait(false); + + return destination is null + ? throw new ConfigurationException($"Specified destination '{destinationName}' does not exist.") + : destination; } - protected virtual void Dispose(bool disposing) + protected virtual async Task GetDestination(ExportRequestDataMessage exportRequestData, string destinationName, CancellationToken cancellationToken) { - if (!_disposedValue) + try { - if (disposing) + return await LookupDestinationAsync(destinationName, cancellationToken).ConfigureAwait(false); + } + catch (ConfigurationException ex) + { + HandleCStoreException(ex, exportRequestData); + return null; + } + } + + protected virtual async Task HandleDesination(ExportRequestDataMessage exportRequestData, string destinationName, CancellationToken cancellationToken) + { + Guard.Against.Null(exportRequestData, nameof(exportRequestData)); + + var manualResetEvent = new ManualResetEvent(false); + var destination = await GetDestination(exportRequestData, destinationName, cancellationToken).ConfigureAwait(false); + if (destination is null) + { + return; + } + + try + { + await ExecuteExport(exportRequestData, manualResetEvent, destination!, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + HandleCStoreException(ex, exportRequestData); + } + } + + private async Task GenerateRequestsAsync( + ExportRequestDataMessage exportRequestData, + IDicomClient client, + ManualResetEvent manualResetEvent) + { + DicomFile dicomFile; + try + { + dicomFile = _dicomToolkit.Load(exportRequestData.FileContent); + } + catch (Exception ex) + { + var errorMessage = $"Error reading DICOM file: {ex.Message}"; + _logger.ExportException(errorMessage, ex); + exportRequestData.SetFailed(FileExportStatus.UnsupportedDataType, errorMessage); + return false; + } + + try + { + var request = new DicomCStoreRequest(dicomFile); + + request.OnResponseReceived += (req, response) => { - _scope.Dispose(); + if (response.Status == DicomStatus.Success) + { + _logger.DimseExportInstanceComplete(); + } + else + { + var errorMessage = $"Failed to export with error {response.Status}"; + _logger.DimseExportInstanceError(response.Status); + exportRequestData.SetFailed(FileExportStatus.ServiceError, errorMessage); + } + manualResetEvent.Set(); + }; + + await client.AddRequestAsync(request).ConfigureAwait(false); + return true; + } + catch (Exception exception) + { + var errorMessage = $"Error while adding DICOM C-STORE request: {exception.Message}"; + _logger.DimseExportErrorAddingInstance(exception.Message, exception); + exportRequestData.SetFailed(FileExportStatus.ServiceError, errorMessage); + return false; + } + } + + protected async Task ExecuteExport(ExportRequestDataMessage exportRequestData, ManualResetEvent manualResetEvent, DestinationApplicationEntity destination, CancellationToken cancellationToken) => await Policy + .Handle() + .WaitAndRetryAsync( + _configuration.Export.Retries.RetryDelays, + (exception, timeSpan, retryCount, context) => + { + _logger.DimseExportErrorWithRetry(timeSpan, retryCount, exception); + }) + .ExecuteAsync(async () => + { + var client = DicomClientFactory.Create( + destination.HostIp, + destination.Port, + false, + _configuration.Dicom.Scu.AeTitle, + destination.AeTitle); + + client.AssociationAccepted += (sender, args) => _logger.ExportAssociationAccepted(); + client.AssociationRejected += (sender, args) => _logger.ExportAssociationRejected(); + client.AssociationReleased += (sender, args) => _logger.ExportAssociationReleased(); + client.ServiceOptions.LogDataPDUs = _configuration.Dicom.Scu.LogDataPdus; + client.ServiceOptions.LogDimseDatasets = _configuration.Dicom.Scu.LogDimseDatasets; + + client.NegotiateAsyncOps(); + if (await GenerateRequestsAsync(exportRequestData, client, manualResetEvent).ConfigureAwait(false)) + { + _logger.DimseExporting(destination.AeTitle, destination.HostIp, destination.Port); + await client.SendAsync(cancellationToken).ConfigureAwait(false); + manualResetEvent.WaitOne(); + _logger.DimseExportComplete(destination.AeTitle); + } + }).ConfigureAwait(false); + + protected async Task BaseProcessMessage(MessageReceivedEventArgs eventArgs) + { + var (exportFlow, reportingActionBlock) = SetupActionBlocks(); + + lock (SyncRoot) + { + var exportRequest = eventArgs.Message.ConvertTo(); + if (ExportRequests.ContainsKey(exportRequest.ExportTaskId)) + { + _logger.ExportRequestAlreadyQueued(exportRequest.CorrelationId, exportRequest.ExportTaskId); + return; } - _disposedValue = true; + exportRequest.MessageId = eventArgs.Message.MessageId; + exportRequest.DeliveryTag = eventArgs.Message.DeliveryTag; + + var exportRequestWithDetails = new ExportRequestEventDetails(exportRequest); + + ExportRequests.Add(exportRequest.ExportTaskId, exportRequestWithDetails); + if (!exportFlow.Post(exportRequestWithDetails)) + { + _logger.ErrorPostingExportJobToQueue(exportRequest.CorrelationId, exportRequest.ExportTaskId); + MessageSubscriber.Reject(eventArgs.Message); + } + else + { + _logger.ExportRequestQueuedForProcessing(exportRequest.CorrelationId, exportRequest.MessageId, exportRequest.ExportTaskId); + } } + + exportFlow.Complete(); + await reportingActionBlock.Completion.ConfigureAwait(false); + } public void Dispose() diff --git a/src/InformaticsGateway/Services/Export/ExtAppScuExportService.cs b/src/InformaticsGateway/Services/Export/ExtAppScuExportService.cs new file mode 100755 index 000000000..fb602be5d --- /dev/null +++ b/src/InformaticsGateway/Services/Export/ExtAppScuExportService.cs @@ -0,0 +1,166 @@ +/* + * Copyright 2021-2022 MONAI Consortium + * Copyright 2019-2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using FellowOakDicom; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api.Models; +using Monai.Deploy.InformaticsGateway.Common; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Monai.Deploy.InformaticsGateway.Logging; +using Monai.Deploy.Messaging.Common; +using Monai.Deploy.Messaging.Events; + +namespace Monai.Deploy.InformaticsGateway.Services.Export +{ + public class ExtAppScuExportService : ExportServiceBase + { + private readonly ILogger _logger; + private readonly IOptions _configuration; + private readonly IExternalAppDetailsRepository _repository; + private readonly IDicomToolkit _dicomToolkit; + protected override ushort Concurrency { get; } + public override string RoutingKey { get; } + public override string ServiceName => "External App Export Service"; + + public ExtAppScuExportService( + ILogger logger, + IServiceScopeFactory serviceScopeFactory, + IOptions configuration, + IDicomToolkit dicomToolkit, + IExternalAppDetailsRepository repository) + : base(logger, configuration, serviceScopeFactory, dicomToolkit) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + _dicomToolkit = dicomToolkit ?? throw new ArgumentNullException(nameof(dicomToolkit)); + RoutingKey = $"{configuration.Value.Messaging.Topics.ExternalAppRequest}"; + Concurrency = _configuration.Value.Dicom.Scu.MaximumNumberOfAssociations; + } + + protected override async Task ProcessMessage(MessageReceivedEventArgs eventArgs) + { + var (exportFlow, reportingActionBlock) = SetupActionBlocks(); + + lock (SyncRoot) + { + var externalAppRequest = eventArgs.Message.ConvertTo(); + if (ExportRequests.ContainsKey(externalAppRequest.ExportTaskId)) + { + _logger.ExportRequestAlreadyQueued(externalAppRequest.CorrelationId, externalAppRequest.ExportTaskId); + return; + } + + externalAppRequest.MessageId = eventArgs.Message.MessageId; + externalAppRequest.DeliveryTag = eventArgs.Message.DeliveryTag; + + var exportRequestWithDetails = new ExportRequestEventDetails(externalAppRequest); + + ExportRequests.Add(externalAppRequest.ExportTaskId, exportRequestWithDetails); + if (!exportFlow.Post(exportRequestWithDetails)) + { + _logger.ErrorPostingExportJobToQueue(externalAppRequest.CorrelationId, externalAppRequest.ExportTaskId); + MessageSubscriber.Reject(eventArgs.Message); + } + else + { + _logger.ExportRequestQueuedForProcessing(externalAppRequest.CorrelationId, externalAppRequest.MessageId, externalAppRequest.ExportTaskId); + } + } + + exportFlow.Complete(); + await reportingActionBlock.Completion.ConfigureAwait(false); + } + + protected override async Task ExportCompleteCallback(ExportRequestDataMessage exportRequestData) + { + try + { + var dicom = _dicomToolkit.Load(exportRequestData.FileContent); + dicom.Dataset.TryGetString(DicomTag.PatientID, out var patientID); + if (dicom.Dataset.TryGetString(DicomTag.StudyInstanceUID, out var studyInstUID)) + { + var (newStudyInstanceUID, newPatientId) = + await SaveInRepo(exportRequestData, studyInstUID, patientID ?? string.Empty) + .ConfigureAwait(false); + dicom.Dataset.AddOrUpdate(DicomTag.StudyInstanceUID, newStudyInstanceUID); + dicom.Dataset.AddOrUpdate(DicomTag.PatientID, newPatientId); + + using var ms = new MemoryStream(); + await dicom.SaveAsync(ms).ConfigureAwait(false); + exportRequestData.SetData(ms.ToArray()); + return; + } + throw new ExternalAppExeception("No StudyInstanceUID tag found"); + } + catch (Exception ex) + { + var errorMessage = $"Error reading DICOM file: {ex.Message}"; + _logger.ExportException(errorMessage, ex); + exportRequestData.SetFailed(FileExportStatus.UnsupportedDataType, errorMessage); + } + + } + + private async Task<(string, string)> SaveInRepo(ExportRequestDataMessage externalAppRequest, string studyinstanceId, string patientId) + { + var existing = (await _repository.GetAsync(studyinstanceId, new CancellationToken()).ConfigureAwait(false)) + ?.Find(e => e.WorkflowInstanceId == externalAppRequest.WorkflowInstanceId && + e.ExportTaskID == externalAppRequest.ExportTaskId); + if (existing is null) + { + var studyInstanceUidOutBound = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var PatientIdOutbound = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + await _repository.AddAsync(new ExternalAppDetails + { + StudyInstanceUid = studyinstanceId, + StudyInstanceUidOutBound = studyInstanceUidOutBound, + WorkflowInstanceId = externalAppRequest.WorkflowInstanceId, + ExportTaskID = externalAppRequest.ExportTaskId, + CorrelationId = externalAppRequest.CorrelationId, + DateTimeCreated = DateTime.Now, + DestinationFolder = externalAppRequest.FilePayloadId, + PatientId = patientId, + PatientIdOutBound = PatientIdOutbound + }, new CancellationToken()).ConfigureAwait(false); + _logger.SavingExternalAppData(studyinstanceId); + return (studyInstanceUidOutBound, PatientIdOutbound); + } + return (existing.StudyInstanceUidOutBound, existing.PatientIdOutBound); + } + + protected override async Task ExportDataBlockCallback(ExportRequestDataMessage exportRequestData, CancellationToken cancellationToken) + { + using var loggerScope = _logger.BeginScope(new Messaging.Common.LoggingDataDictionary { { "ExportTaskId", exportRequestData.ExportTaskId }, { "CorrelationId", exportRequestData.CorrelationId }, { "Filename", exportRequestData.Filename } }); + + foreach (var destinationName in exportRequestData.Destinations) + { + await HandleDesination(exportRequestData, destinationName, cancellationToken).ConfigureAwait(false); + } + + return exportRequestData; + } + } +} diff --git a/src/InformaticsGateway/Services/Export/ExternalAppExeception.cs b/src/InformaticsGateway/Services/Export/ExternalAppExeception.cs new file mode 100755 index 000000000..0b2860011 --- /dev/null +++ b/src/InformaticsGateway/Services/Export/ExternalAppExeception.cs @@ -0,0 +1,40 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.Runtime.Serialization; + +namespace Monai.Deploy.InformaticsGateway.Services.Export +{ + public class ExternalAppExeception : Exception + { + public ExternalAppExeception() + { + } + + public ExternalAppExeception(string message) : base(message) + { + } + + public ExternalAppExeception(string message, Exception innerException) : base(message, innerException) + { + } + + protected ExternalAppExeception(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/InformaticsGateway/Services/Export/Hl7ExportService.cs b/src/InformaticsGateway/Services/Export/Hl7ExportService.cs new file mode 100755 index 000000000..0e39217d1 --- /dev/null +++ b/src/InformaticsGateway/Services/Export/Hl7ExportService.cs @@ -0,0 +1,164 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Ardalis.GuardClauses; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api.Models; +using Monai.Deploy.InformaticsGateway.Common; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Monai.Deploy.InformaticsGateway.Logging; +using Monai.Deploy.InformaticsGateway.Api.Mllp; +using Monai.Deploy.Messaging.Common; +using Polly; + +namespace Monai.Deploy.InformaticsGateway.Services.Export +{ + internal class Hl7ExportService : ExportServiceBase + { + private readonly ILogger _logger; + private readonly InformaticsGatewayConfiguration _configuration; + private readonly IMllpService _mllpService; + + protected override ushort Concurrency { get; } + public override string RoutingKey { get; } + public override string ServiceName => "DICOM Export HL7 Service"; + + + public Hl7ExportService( + ILogger logger, + IServiceScopeFactory serviceScopeFactory, + IOptions configuration, + IDicomToolkit dicomToolkit) + : base(logger, configuration, serviceScopeFactory, dicomToolkit) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _configuration = configuration.Value ?? throw new ArgumentNullException(nameof(configuration)); + + _mllpService = serviceScopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + RoutingKey = $"{configuration.Value.Messaging.Topics.ExportHL7}"; + ExportCompleteTopic = $"{configuration.Value.Messaging.Topics.ExportHl7Complete}"; + Concurrency = _configuration.Dicom.Scu.MaximumNumberOfAssociations; + } + + + protected override Task ProcessMessage(MessageReceivedEventArgs eventArgs) + { + return BaseProcessMessage(eventArgs); + } + + + protected override async Task ExportDataBlockCallback(ExportRequestDataMessage exportRequestData, CancellationToken cancellationToken) + { + using var loggerScope = _logger.BeginScope(new Api.LoggingDataDictionary + { + { "ExportTaskId", exportRequestData.ExportTaskId }, + { "CorrelationId", exportRequestData.CorrelationId }, + { "Filename", exportRequestData.Filename } + }); + + foreach (var destinationName in exportRequestData.Destinations) + { + await HandleDesination(exportRequestData, destinationName, cancellationToken).ConfigureAwait(false); + } + + return exportRequestData; + } + + protected override async Task HandleDesination(ExportRequestDataMessage exportRequestData, string destinationName, CancellationToken cancellationToken) + { + Guard.Against.Null(exportRequestData, nameof(exportRequestData)); + + var destination = await GetHL7Destination(exportRequestData, destinationName, cancellationToken).ConfigureAwait(false); + if (destination is null) + { + return; + } + + try + { + await ExecuteHl7Export(exportRequestData, destination!, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + HandleCStoreException(ex, exportRequestData); + } + } + + private async Task ExecuteHl7Export( + ExportRequestDataMessage exportRequestData, + HL7DestinationEntity destination, + CancellationToken cancellationToken) => await Policy + .Handle() + .WaitAndRetryAsync( + _configuration.Export.Retries.RetryDelays, + (exception, timeSpan, retryCount, context) => + { + _logger.HL7ExportErrorWithRetry(timeSpan, retryCount, exception); + }) + .ExecuteAsync(async () => + { + await _mllpService.SendMllp( + IPAddress.Parse(destination.HostIp), + destination.Port, Encoding.UTF8.GetString(exportRequestData.FileContent), + cancellationToken + ).ConfigureAwait(false); + }).ConfigureAwait(false); + + + private async Task LookupDestinationAsync(string destinationName, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(destinationName)) + { + throw new ConfigurationException("Export task does not have destination set."); + } + + using var scope = ServiceScopeFactory.CreateScope(); + var repository = scope.ServiceProvider.GetRequiredService(); + var destination = await repository.FindByNameAsync(destinationName, cancellationToken).ConfigureAwait(false); + + return destination is null + ? throw new ConfigurationException($"Specified destination '{destinationName}' does not exist.") + : destination; + } + + private async Task GetHL7Destination(ExportRequestDataMessage exportRequestData, string destinationName, CancellationToken cancellationToken) + { + try + { + return await LookupDestinationAsync(destinationName, cancellationToken).ConfigureAwait(false); + } + catch (ConfigurationException ex) + { + HandleCStoreException(ex, exportRequestData); + return null; + } + } + + protected override Task ExecuteOutputDataEngineCallback(ExportRequestDataMessage exportDataRequest) + { + return Task.FromResult(exportDataRequest); + } + + } +} diff --git a/src/InformaticsGateway/Services/Export/ScuExportService.cs b/src/InformaticsGateway/Services/Export/ScuExportService.cs index 1daec61eb..09d47a48a 100755 --- a/src/InformaticsGateway/Services/Export/ScuExportService.cs +++ b/src/InformaticsGateway/Services/Export/ScuExportService.cs @@ -16,32 +16,25 @@ */ using System; -using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; -using Ardalis.GuardClauses; -using FellowOakDicom; -using FellowOakDicom.Network; -using FellowOakDicom.Network.Client; +using System.Threading.Tasks.Dataflow; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; -using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; using Monai.Deploy.InformaticsGateway.Logging; +using Monai.Deploy.Messaging.Common; using Monai.Deploy.Messaging.Events; -using Polly; namespace Monai.Deploy.InformaticsGateway.Services.Export { internal class ScuExportService : ExportServiceBase { private readonly ILogger _logger; - private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IOptions _configuration; - private readonly IDicomToolkit _dicomToolkit; protected override ushort Concurrency { get; } public override string RoutingKey { get; } @@ -52,20 +45,23 @@ public ScuExportService( IServiceScopeFactory serviceScopeFactory, IOptions configuration, IDicomToolkit dicomToolkit) - : base(logger, configuration, serviceScopeFactory) + : base(logger, configuration, serviceScopeFactory, dicomToolkit) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - _dicomToolkit = dicomToolkit ?? throw new ArgumentNullException(nameof(dicomToolkit)); RoutingKey = $"{configuration.Value.Messaging.Topics.ExportRequestPrefix}.{_configuration.Value.Dicom.Scu.AgentName}"; Concurrency = _configuration.Value.Dicom.Scu.MaximumNumberOfAssociations; } + protected override async Task ProcessMessage(MessageReceivedEventArgs eventArgs) + { + await BaseProcessMessage(eventArgs); + } + protected override async Task ExportDataBlockCallback(ExportRequestDataMessage exportRequestData, CancellationToken cancellationToken) { - using var loggerScope = _logger.BeginScope(new LoggingDataDictionary { { "ExportTaskId", exportRequestData.ExportTaskId }, { "CorrelationId", exportRequestData.CorrelationId }, { "Filename", exportRequestData.Filename } }); + using var loggerScope = _logger.BeginScope(new Api.LoggingDataDictionary { { "ExportTaskId", exportRequestData.ExportTaskId }, { "CorrelationId", exportRequestData.CorrelationId }, { "Filename", exportRequestData.Filename } }); foreach (var destinationName in exportRequestData.Destinations) { @@ -74,159 +70,5 @@ protected override async Task ExportDataBlockCallback( return exportRequestData; } - - private async Task HandleDesination(ExportRequestDataMessage exportRequestData, string destinationName, CancellationToken cancellationToken) - { - Guard.Against.Null(exportRequestData, nameof(exportRequestData)); - - var manualResetEvent = new ManualResetEvent(false); - DestinationApplicationEntity? destination = null; - try - { - destination = await LookupDestinationAsync(destinationName, cancellationToken).ConfigureAwait(false); - } - catch (ConfigurationException ex) - { - _logger.ScuExportConfigurationError(ex.Message, ex); - exportRequestData.SetFailed(FileExportStatus.ConfigurationError, ex.Message); - return; - } - - try - { - await Policy - .Handle() - .WaitAndRetryAsync( - _configuration.Value.Export.Retries.RetryDelays, - (exception, timeSpan, retryCount, context) => - { - _logger.DimseExportErrorWithRetry(timeSpan, retryCount, exception); - }) - .ExecuteAsync(async () => - { - var client = DicomClientFactory.Create( - destination.HostIp, - destination.Port, - false, - _configuration.Value.Dicom.Scu.AeTitle, - destination.AeTitle); - - client.AssociationAccepted += (sender, args) => _logger.ExportAssociationAccepted(); - client.AssociationRejected += (sender, args) => _logger.ExportAssociationRejected(); - client.AssociationReleased += (sender, args) => _logger.ExportAssociationReleased(); - client.ServiceOptions.LogDataPDUs = _configuration.Value.Dicom.Scu.LogDataPdus; - client.ServiceOptions.LogDimseDatasets = _configuration.Value.Dicom.Scu.LogDimseDatasets; - - client.NegotiateAsyncOps(); - if (await GenerateRequestsAsync(exportRequestData, client, manualResetEvent).ConfigureAwait(false)) - { - _logger.DimseExporting(destination.AeTitle, destination.HostIp, destination.Port); - await client.SendAsync(cancellationToken).ConfigureAwait(false); - manualResetEvent.WaitOne(); - _logger.DimseExportComplete(destination.AeTitle); - } - }).ConfigureAwait(false); - } - catch (Exception ex) - { - HandleCStoreException(ex, exportRequestData); - } - } - - private async Task LookupDestinationAsync(string destinationName, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(destinationName)) - { - throw new ConfigurationException("Export task does not have destination set."); - } - - using var scope = _serviceScopeFactory.CreateScope(); - var repository = scope.ServiceProvider.GetRequiredService(); - var destination = await repository.FindByNameAsync(destinationName, cancellationToken).ConfigureAwait(false); - - if (destination is null) - { - throw new ConfigurationException($"Specified destination '{destinationName}' does not exist."); - } - - return destination; - } - - private async Task GenerateRequestsAsync( - ExportRequestDataMessage exportRequestData, - IDicomClient client, - ManualResetEvent manualResetEvent) - { - DicomFile dicomFile; - try - { - dicomFile = _dicomToolkit.Load(exportRequestData.FileContent); - } - catch (Exception ex) - { - var errorMessage = $"Error reading DICOM file: {ex.Message}"; - _logger.ExportException(errorMessage, ex); - exportRequestData.SetFailed(FileExportStatus.UnsupportedDataType, errorMessage); - return false; - } - - try - { - var request = new DicomCStoreRequest(dicomFile); - - request.OnResponseReceived += (req, response) => - { - if (response.Status == DicomStatus.Success) - { - _logger.DimseExportInstanceComplete(); - } - else - { - var errorMessage = $"Failed to export with error {response.Status}"; - _logger.DimseExportInstanceError(response.Status); - exportRequestData.SetFailed(FileExportStatus.ServiceError, errorMessage); - } - manualResetEvent.Set(); - }; - - await client.AddRequestAsync(request).ConfigureAwait(false); - return true; - } - catch (Exception exception) - { - var errorMessage = $"Error while adding DICOM C-STORE request: {exception.Message}"; - _logger.DimseExportErrorAddingInstance(exception.Message, exception); - exportRequestData.SetFailed(FileExportStatus.ServiceError, errorMessage); - return false; - } - } - - private void HandleCStoreException(Exception ex, ExportRequestDataMessage exportRequestData) - { - var exception = ex; - - if (exception is AggregateException) - { - exception = exception.InnerException!; - } - - var errorMessage = $"Job failed with error: {exception.Message}."; - - if (exception is DicomAssociationAbortedException abortEx) - { - errorMessage = $"Association aborted with reason {abortEx.AbortReason}."; - } - else if (exception is DicomAssociationRejectedException rejectEx) - { - errorMessage = $"Association rejected with reason {rejectEx.RejectReason}."; - } - else if (exception is SocketException socketException) - { - errorMessage = $"Association aborted with error {socketException.Message}."; - } - - _logger.ExportException(errorMessage, ex); - exportRequestData.SetFailed(FileExportStatus.ServiceError, errorMessage); - } } } diff --git a/src/InformaticsGateway/Services/HealthLevel7/Hl7SendException.cs b/src/InformaticsGateway/Services/HealthLevel7/Hl7SendException.cs new file mode 100755 index 000000000..ebb73e6aa --- /dev/null +++ b/src/InformaticsGateway/Services/HealthLevel7/Hl7SendException.cs @@ -0,0 +1,40 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.Runtime.Serialization; + +namespace Monai.Deploy.InformaticsGateway.Services.HealthLevel7 +{ + internal class Hl7SendException : Exception + { + public Hl7SendException() + { + } + + public Hl7SendException(string message) : base(message) + { + } + + public Hl7SendException(string message, Exception innerException) : base(message, innerException) + { + } + + protected Hl7SendException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/InformaticsGateway/Services/HealthLevel7/IMllpClientFactory.cs b/src/InformaticsGateway/Services/HealthLevel7/IMllpClientFactory.cs old mode 100644 new mode 100755 index e2d145b6e..3640227a2 --- a/src/InformaticsGateway/Services/HealthLevel7/IMllpClientFactory.cs +++ b/src/InformaticsGateway/Services/HealthLevel7/IMllpClientFactory.cs @@ -18,7 +18,7 @@ using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Services.Common; -namespace Monai.Deploy.InformaticsGateway.Services.HealthLevel7 +namespace Monai.Deploy.InformaticsGateway.Api.Mllp { internal interface IMllpClientFactory { @@ -27,6 +27,7 @@ internal interface IMllpClientFactory internal class MllpClientFactory : IMllpClientFactory { - public IMllpClient CreateClient(ITcpClientAdapter client, Hl7Configuration configurations, ILogger logger) => new MllpClient(client, configurations, logger); + public IMllpClient CreateClient(ITcpClientAdapter client, Hl7Configuration configurations, ILogger logger) + => new MllpClient(client, configurations, logger); } } diff --git a/src/InformaticsGateway/Services/HealthLevel7/MllpClient.cs b/src/InformaticsGateway/Services/HealthLevel7/MllpClient.cs old mode 100644 new mode 100755 index 0a79c8d1f..520043b0d --- a/src/InformaticsGateway/Services/HealthLevel7/MllpClient.cs +++ b/src/InformaticsGateway/Services/HealthLevel7/MllpClient.cs @@ -23,12 +23,12 @@ using Ardalis.GuardClauses; using HL7.Dotnetcore; using Microsoft.Extensions.Logging; -using Monai.Deploy.InformaticsGateway.Api; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Logging; using Monai.Deploy.InformaticsGateway.Services.Common; +using Monai.Deploy.InformaticsGateway.Services.HealthLevel7; -namespace Monai.Deploy.InformaticsGateway.Services.HealthLevel7 +namespace Monai.Deploy.InformaticsGateway.Api.Mllp { internal sealed class MllpClient : IMllpClient { @@ -143,6 +143,7 @@ private async Task> ReceiveData(INetworkStream clientStream, Canc } } while (true); } + linkedCancellationTokenSource.Dispose(); return messages; } @@ -247,4 +248,4 @@ public void Dispose() GC.SuppressFinalize(this); } } -} \ No newline at end of file +} diff --git a/src/InformaticsGateway/Services/HealthLevel7/MllpExtract.cs b/src/InformaticsGateway/Services/HealthLevel7/MllpExtract.cs new file mode 100755 index 000000000..7e9ed24d3 --- /dev/null +++ b/src/InformaticsGateway/Services/HealthLevel7/MllpExtract.cs @@ -0,0 +1,165 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using FellowOakDicom; +using HL7.Dotnetcore; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api.Models; +using Monai.Deploy.InformaticsGateway.Api.Storage; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Monai.Deploy.InformaticsGateway.Logging; + +namespace Monai.Deploy.InformaticsGateway.Api.Mllp +{ + public sealed class MllpExtract : IMllpExtract + { + private readonly ILogger _logger; + private readonly IHl7ApplicationConfigRepository _hl7ApplicationConfigRepository; + private readonly IExternalAppDetailsRepository _externalAppDetailsRepository; + + public MllpExtract(IHl7ApplicationConfigRepository hl7ApplicationConfigRepository, IExternalAppDetailsRepository externalAppDetailsRepository, ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _hl7ApplicationConfigRepository = hl7ApplicationConfigRepository ?? throw new ArgumentNullException(nameof(hl7ApplicationConfigRepository)); + _externalAppDetailsRepository = externalAppDetailsRepository ?? throw new ArgumentNullException(nameof(externalAppDetailsRepository)); + } + + + public async Task ExtractInfo(Hl7FileStorageMetadata meta, Message message, Hl7ApplicationConfigEntity configItem) + { + try + { + // extract data for the given fields + // Use Id to get record from Db + var details = await GetExtAppDetails(configItem, message).ConfigureAwait(false); + + if (details is null) + { + _logger.Hl7ExtAppDetailsNotFound(); + return message; + } + + // fill in meta data with workflowInstance and Task ID + // repopulate message with data from record + + meta.WorkflowInstanceId = details.WorkflowInstanceId; + meta.TaskId = details.ExportTaskID; + meta.ChangeCorrelationId(_logger, details.CorrelationId); + + if (string.IsNullOrEmpty(details.DestinationFolder) is false) + { + meta.File.DestinationFolderOverride = true; + meta.File.UploadPath = $"{details.DestinationFolder}/{meta.DataTypeDirectoryName}/{meta.Id}{Hl7FileStorageMetadata.FileExtension}"; + } + message = RepopulateMessage(configItem, details, message); + } + catch (Exception ex) + { + _logger.Hl7ExceptionThrow(ex); + } + return message; + } + + public async Task GetConfigItem(Message message) + { + // load the config + var config = await _hl7ApplicationConfigRepository.GetAllAsync().ConfigureAwait(false); + if (config == null) + { + _logger.Hl7NoConfig(); + return null; + } + _logger.Hl7ConfigLoaded($"Config: {config}"); + // get config for vendorId + var configItem = GetConfig(config, message); + if (configItem == null) + { + _logger.Hl7NoMatchingConfig(message.HL7Message); + return null; + } + return configItem; + } + + private async Task GetExtAppDetails(Hl7ApplicationConfigEntity hl7ApplicationConfigEntity, Message message) + { + var tagId = message.GetValue(hl7ApplicationConfigEntity.DataLink.Key); + var type = hl7ApplicationConfigEntity.DataLink.Value; + switch (type) + { + case DataLinkType.PatientId: + return await _externalAppDetailsRepository.GetByPatientIdOutboundAsync(tagId, new CancellationToken()).ConfigureAwait(false); ; + case DataLinkType.StudyInstanceUid: + return await _externalAppDetailsRepository.GetByStudyIdOutboundAsync(tagId, new CancellationToken()).ConfigureAwait(false); ; + default: + break; + } + + throw new Exception($"Invalid DataLinkType: {type}"); + } + + internal static Hl7ApplicationConfigEntity? GetConfig(List config, Message message) + { + foreach (var item in config) + { + var t = message.GetValue(item.SendingId.Key); + if (item.SendingId.Value == message.GetValue(item.SendingId.Key)) + { + return item; + } + } + return null; + } + + private Message RepopulateMessage(Hl7ApplicationConfigEntity config, ExternalAppDetails details, Message message) + { + foreach (var item in config.DataMapping) + { + var tag = DicomTag.Parse(item.Value); + // these are the only two fields we have at the point + if (tag == DicomTag.PatientID) + { + var oldvalue = message.GetValue(item.Key); + message.SetValue(item.Key, details.PatientId); + _logger.ChangingHl7Values(item.Key, oldvalue, details.PatientId); + if (message.HL7Message.Contains(oldvalue)) + { + var newMess = message.HL7Message.Replace(oldvalue, details.PatientId); + message = new Message(newMess); + message.ParseMessage(); + } + } + else if (tag == DicomTag.StudyInstanceUID) + { + var oldvalue = message.GetValue(item.Key); + message.SetValue(item.Key, details.StudyInstanceUid); + _logger.ChangingHl7Values(item.Key, oldvalue, details.StudyInstanceUid); + if (message.HL7Message.Contains(oldvalue)) + { + var newMess = message.HL7Message.Replace(oldvalue, details.StudyInstanceUid); + message = new Message(newMess); + message.ParseMessage(); + } + } + } + return message; + } + } +} diff --git a/src/InformaticsGateway/Services/HealthLevel7/MllpService.cs b/src/InformaticsGateway/Services/HealthLevel7/MllpService.cs index 24f7269cf..7f247c1b2 100755 --- a/src/InformaticsGateway/Services/HealthLevel7/MllpService.cs +++ b/src/InformaticsGateway/Services/HealthLevel7/MllpService.cs @@ -18,26 +18,34 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO.Abstractions; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; +using HL7.Dotnetcore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Api.Rest; using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; using Monai.Deploy.InformaticsGateway.Logging; using Monai.Deploy.InformaticsGateway.Services.Common; using Monai.Deploy.InformaticsGateway.Services.Connectors; +using Monai.Deploy.InformaticsGateway.Services.HealthLevel7; using Monai.Deploy.InformaticsGateway.Services.Storage; using Monai.Deploy.Messaging.Events; -namespace Monai.Deploy.InformaticsGateway.Services.HealthLevel7 +namespace Monai.Deploy.InformaticsGateway.Api.Mllp { - internal sealed class MllpService : IHostedService, IDisposable, IMonaiService + internal sealed class MllpService : IMllpService, IHostedService, IDisposable, IMonaiService { private const int SOCKET_OPERATION_CANCELLED = 125; private bool _disposedValue; @@ -51,6 +59,10 @@ internal sealed class MllpService : IHostedService, IDisposable, IMonaiService private readonly IOptions _configuration; private readonly IStorageInfoProvider _storageInfoProvider; private readonly ConcurrentDictionary _activeTasks; + private readonly IMllpExtract _mIIpExtract; + private readonly IInputHL7DataPlugInEngine _inputHL7DataPlugInEngine; + private readonly IHl7ApplicationConfigRepository _hl7ApplicationConfigRepository; + private DateTime _lastConfigRead = new(2000, 1, 1); public int ActiveConnections { @@ -84,7 +96,10 @@ public MllpService(IServiceScopeFactory serviceScopeFactory, _payloadAssembler = serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IPayloadAssembler)); _fileSystem = serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IFileSystem)); _storageInfoProvider = serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IStorageInfoProvider)); + _mIIpExtract = serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IMllpExtract)); _activeTasks = new ConcurrentDictionary(); + _inputHL7DataPlugInEngine = serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IInputHL7DataPlugInEngine)); + _hl7ApplicationConfigRepository = serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IHl7ApplicationConfigRepository)); } public Task StartAsync(CancellationToken cancellationToken) @@ -164,15 +179,25 @@ private async Task OnDisconnect(IMllpClient client, MllpClientResult result) Guard.Against.Null(client, nameof(client)); Guard.Against.Null(result, nameof(result)); + await ConfigurePlugInEngine().ConfigureAwait(false); + try { foreach (var message in result.Messages) { - var hl7Fileetadata = new Hl7FileStorageMetadata(client.ClientId.ToString(), DataService.HL7, client.ClientIp); - await hl7Fileetadata.SetDataStream(message.HL7Message, _configuration.Value.Storage.TemporaryDataStorage, _fileSystem, _configuration.Value.Storage.LocalTemporaryStoragePath).ConfigureAwait(false); - var payloadId = await _payloadAssembler.Queue(client.ClientId.ToString(), hl7Fileetadata, new DataOrigin { DataService = DataService.HL7, Source = client.ClientIp, Destination = FileStorageMetadata.IpAddress() }).ConfigureAwait(false); - hl7Fileetadata.PayloadId = payloadId.ToString(); - _uploadQueue.Queue(hl7Fileetadata); + var newMessage = message; + var hl7Filemetadata = new Hl7FileStorageMetadata(client.ClientId.ToString(), DataService.HL7, client.ClientIp); + var configItem = await _mIIpExtract.GetConfigItem(message).ConfigureAwait(false); + if (configItem is not null) + { + await _inputHL7DataPlugInEngine.ExecutePlugInsAsync(message, hl7Filemetadata, configItem).ConfigureAwait(false); + newMessage = await _mIIpExtract.ExtractInfo(hl7Filemetadata, message, configItem).ConfigureAwait(false); + } + + await hl7Filemetadata.SetDataStream(newMessage.HL7Message, _configuration.Value.Storage.TemporaryDataStorage, _fileSystem, _configuration.Value.Storage.LocalTemporaryStoragePath).ConfigureAwait(false); + var payloadId = await _payloadAssembler.Queue(client.ClientId.ToString(), hl7Filemetadata, new DataOrigin { DataService = DataService.HL7, Source = client.ClientIp, Destination = FileStorageMetadata.IpAddress() }).ConfigureAwait(false); + hl7Filemetadata.PayloadId ??= payloadId.ToString(); + _uploadQueue.Queue(hl7Filemetadata); } } catch (Exception ex) @@ -187,6 +212,32 @@ private async Task OnDisconnect(IMllpClient client, MllpClientResult result) } } + private async Task ConfigurePlugInEngine() + { + var configs = await _hl7ApplicationConfigRepository.GetAllAsync().ConfigureAwait(false); + if (configs is not null && configs.Max(c => c.LastModified) > _lastConfigRead) + { + var pluginAssemblies = new List(); + foreach (var config in configs.Where(p => p.PlugInAssemblies is not null && p.PlugInAssemblies.Count > 0)) + { + try + { + var addMe = config.PlugInAssemblies.Where(p => pluginAssemblies.Any(a => a == p) is false); + pluginAssemblies.AddRange(config.PlugInAssemblies.Where(p => pluginAssemblies.Any(a => a == p) is false)); + } + catch (Exception ex) + { + _logger.HL7PluginLoadingExceptions(ex); + } + } + if (pluginAssemblies.Any()) + { + _inputHL7DataPlugInEngine.Configure(pluginAssemblies); + } + } + _lastConfigRead = DateTime.UtcNow; + } + private void WaitUntilAvailable(int maximumNumberOfConnections) { var count = 0; @@ -216,6 +267,87 @@ private void Dispose(bool disposing) } } + public async Task SendMllp(IPAddress address, int port, string hl7Message, CancellationToken cancellationToken) + { + try + { + var body = $"{Resources.AsciiVT}{hl7Message}{Resources.AsciiFS}{Resources.AcsiiCR}"; + var sendMessageByteBuffer = Encoding.UTF8.GetBytes(body); + await WriteMessage(sendMessageByteBuffer, address, port).ConfigureAwait(false); + } + catch (ArgumentOutOfRangeException) + { + _logger.Hl7AckMissingStartOrEndCharacters(); + throw new Hl7SendException("ACK missing start or end characters"); + } + catch (Exception ex) + { + _logger.Hl7SendException(ex); + throw new Hl7SendException("Send exception"); + } + } + + private async Task WriteMessage(byte[] sendMessageByteBuffer, IPAddress address, int port) + { + + using var tcpClient = new TcpClient(); + + tcpClient.Connect(address, port); + + var networkStream = new NetworkStream(tcpClient.Client); + + if (networkStream.CanWrite) + { + networkStream.Write(sendMessageByteBuffer, 0, sendMessageByteBuffer.Length); + networkStream.Flush(); + } + else + { + _logger.Hl7ClientStreamNotWritable(); + throw new Hl7SendException("Client stream not writable"); + } + + _logger.Hl7MessageSent(Encoding.UTF8.GetString(sendMessageByteBuffer)); + + await EnsureAck(networkStream).ConfigureAwait(false); + } + + private async Task EnsureAck(NetworkStream networkStream) + { + using var s_cts = new CancellationTokenSource(); + s_cts.CancelAfter(_configuration.Value.Hl7.ClientTimeoutMilliseconds); + var buffer = new byte[2048]; + + // get the SentHl7Message + networkStream.ReadTimeout = 5000; + networkStream.WriteTimeout = 5000; + + // wait for responce + while (!s_cts.IsCancellationRequested && networkStream.DataAvailable == false) + { + await Task.Delay(20).ConfigureAwait(false); + } + + var bytesRead = await networkStream.ReadAsync(buffer).ConfigureAwait(false); + + if (bytesRead == 0 || s_cts.IsCancellationRequested) + { + throw new Hl7SendException("ACK message contains no ACK!"); + } + + var _rawHl7Messages = MessageHelper.ExtractMessages(Encoding.UTF8.GetString(buffer)); + foreach (var message in _rawHl7Messages) + { + var hl7Message = new Message(message); + hl7Message.ParseMessage(); + if (hl7Message.MessageStructure == "ACK") + { + return; + } + } + throw new Hl7SendException("ACK message contains no ACK!"); + } + public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method @@ -223,4 +355,4 @@ public void Dispose() GC.SuppressFinalize(this); } } -} \ No newline at end of file +} diff --git a/src/InformaticsGateway/Services/HealthLevel7/Resources.cs b/src/InformaticsGateway/Services/HealthLevel7/Resources.cs old mode 100644 new mode 100755 index 62b0ed23a..f3b30bf3c --- a/src/InformaticsGateway/Services/HealthLevel7/Resources.cs +++ b/src/InformaticsGateway/Services/HealthLevel7/Resources.cs @@ -22,6 +22,7 @@ internal static class Resources public const char AsciiVT = (char)0x0B; public const char AsciiFS = (char)0x1C; + public const char AcsiiCR = (char)13; public const string AcknowledgmentTypeNever = "NE"; public const string AcknowledgmentTypeError = "ER"; diff --git a/src/InformaticsGateway/Services/Http/DestinationAeTitleController.cs b/src/InformaticsGateway/Services/Http/DestinationAeTitleController.cs index 23542973e..91e46723d 100755 --- a/src/InformaticsGateway/Services/Http/DestinationAeTitleController.cs +++ b/src/InformaticsGateway/Services/Http/DestinationAeTitleController.cs @@ -23,6 +23,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; diff --git a/src/InformaticsGateway/Services/Http/DicomAssociationInfoController.cs b/src/InformaticsGateway/Services/Http/DicomAssociationInfoController.cs old mode 100644 new mode 100755 index d794cecff..ad0fb4d37 --- a/src/InformaticsGateway/Services/Http/DicomAssociationInfoController.cs +++ b/src/InformaticsGateway/Services/Http/DicomAssociationInfoController.cs @@ -23,7 +23,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; using Monai.Deploy.InformaticsGateway.Logging; diff --git a/src/InformaticsGateway/Services/Http/HL7DestinationController.cs b/src/InformaticsGateway/Services/Http/HL7DestinationController.cs new file mode 100755 index 000000000..194adb1ed --- /dev/null +++ b/src/InformaticsGateway/Services/Http/HL7DestinationController.cs @@ -0,0 +1,239 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * Copyright 2019-2020 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.Collections.Generic; +using System.Net.Mime; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; +using Monai.Deploy.InformaticsGateway.Common; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Monai.Deploy.InformaticsGateway.Logging; + +namespace Monai.Deploy.InformaticsGateway.Services.Http +{ + [ApiController] + [Route("config/hl7-destination")] + public class HL7DestinationController : ControllerBase + { + private readonly ILogger _logger; + private readonly IHL7DestinationEntityRepository _repository; + + public HL7DestinationController( + ILogger logger, + IHL7DestinationEntityRepository repository) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + + [HttpGet] + [Produces("application/json")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> Get() + { + try + { + return Ok(await _repository.ToListAsync(HttpContext.RequestAborted).ConfigureAwait(false)); + } + catch (Exception ex) + { + _logger.ErrorQueryingDatabase(ex); + return Problem(title: "Error querying database.", statusCode: StatusCodes.Status500InternalServerError, detail: ex.Message); + } + } + + [HttpGet("{name}")] + [Produces("application/json")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ActionName(nameof(GetAeTitle))] + public async Task> GetAeTitle(string name) + { + try + { + var hl7DestinationEntity = await _repository.FindByNameAsync(name, HttpContext.RequestAborted).ConfigureAwait(false); + + if (hl7DestinationEntity is null) + { + return NotFound(); + } + + return Ok(hl7DestinationEntity); + } + catch (Exception ex) + { + _logger.ErrorListingHL7DestinationEntities(ex); + return Problem(title: "Error querying HL7 destinations.", statusCode: StatusCodes.Status500InternalServerError, detail: ex.Message); + } + } + + [HttpGet("cecho/{name}")] + [Produces("application/json")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [ProducesResponseType(StatusCodes.Status502BadGateway)] + [ActionName(nameof(GetAeTitle))] +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + public async Task CEcho(string name) +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + { + throw new NotImplementedException(); + } + + [HttpPost] + [Consumes(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [Produces("application/json")] + public async Task> Create(HL7DestinationEntity item) + { + try + { + item.SetDefaultValues(); + item.SetAuthor(User, EditMode.Create); + + await ValidateCreateAsync(item).ConfigureAwait(false); + + await _repository.AddAsync(item, HttpContext.RequestAborted).ConfigureAwait(false); + _logger.HL7DestinationEntityAdded(item.AeTitle, item.HostIp); + return CreatedAtAction(nameof(GetAeTitle), new { name = item.Name }, item); + } + catch (ObjectExistsException ex) + { + return Problem(title: "HL7 destination already exists", statusCode: StatusCodes.Status409Conflict, detail: ex.Message); + } + catch (ConfigurationException ex) + { + return Problem(title: "Validation error", statusCode: StatusCodes.Status400BadRequest, detail: ex.Message); + } + catch (Exception ex) + { + _logger.ErrorAddingHL7DestinationEntity(ex); + return Problem(title: "Error adding new HL7 destination", statusCode: StatusCodes.Status500InternalServerError, detail: ex.Message); + } + } + + [HttpPut] + [Produces("application/json")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> Edit(HL7DestinationEntity? item) + { + try + { + if (item is null) + { + return NotFound(); + } + + var hl7DestinationEntity = await _repository.FindByNameAsync(item.Name, HttpContext.RequestAborted).ConfigureAwait(false); + if (hl7DestinationEntity is null) + { + return NotFound(); + } + + item.SetDefaultValues(); + + hl7DestinationEntity.AeTitle = item.AeTitle; + hl7DestinationEntity.HostIp = item.HostIp; + hl7DestinationEntity.Port = item.Port; + hl7DestinationEntity.SetAuthor(User, EditMode.Update); + + await ValidateUpdateAsync(hl7DestinationEntity).ConfigureAwait(false); + + _ = _repository.UpdateAsync(hl7DestinationEntity, HttpContext.RequestAborted); + _logger.HL7DestinationEntityUpdated(item.Name, item.AeTitle, item.HostIp, item.Port); + return Ok(hl7DestinationEntity); + } + catch (ConfigurationException ex) + { + return Problem(title: "Validation error.", statusCode: (int)System.Net.HttpStatusCode.BadRequest, detail: ex.Message); + } + catch (Exception ex) + { + _logger.ErrorDeletingHL7DestinationEntity(ex); + return Problem(title: "Error updating HL7 destination.", statusCode: StatusCodes.Status500InternalServerError, detail: ex.Message); + } + } + + [HttpDelete("{name}")] + [Produces("application/json")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> Delete(string name) + { + try + { + var hl7DestinationEntity = await _repository.FindByNameAsync(name, HttpContext.RequestAborted).ConfigureAwait(false); + if (hl7DestinationEntity is null) + { + return NotFound(); + } + + await _repository.RemoveAsync(hl7DestinationEntity, HttpContext.RequestAborted).ConfigureAwait(false); + + _logger.HL7DestinationEntityDeleted(name.Substring(0, 10)); + return Ok(hl7DestinationEntity); + } + catch (Exception ex) + { + _logger.ErrorDeletingHL7DestinationEntity(ex); + return Problem(title: "Error deleting HL7 destination.", statusCode: StatusCodes.Status500InternalServerError, detail: ex.Message); + } + } + + private async Task ValidateCreateAsync(HL7DestinationEntity item) + { + if (await _repository.ContainsAsync(p => p.Name.Equals(item.Name), HttpContext.RequestAborted).ConfigureAwait(false)) + { + throw new ObjectExistsException($"A HL7 destination with the same name '{item.Name}' already exists."); + } + if (await _repository.ContainsAsync(p => p.AeTitle.Equals(item.AeTitle) && p.HostIp.Equals(item.HostIp) && p.Port.Equals(item.Port), HttpContext.RequestAborted).ConfigureAwait(false)) + { + throw new ObjectExistsException($"A HL7 destination with the same AE Title '{item.AeTitle}', host/IP Address '{item.HostIp}' and port '{item.Port}' already exists."); + } + if (!item.IsValid(out var validationErrors)) + { + throw new ConfigurationException(string.Join(Environment.NewLine, validationErrors)); + } + } + + private async Task ValidateUpdateAsync(HL7DestinationEntity item) + { + if (await _repository.ContainsAsync(p => !p.Name.Equals(item.Name) && p.AeTitle.Equals(item.AeTitle) && p.HostIp.Equals(item.HostIp) && p.Port.Equals(item.Port), HttpContext.RequestAborted).ConfigureAwait(false)) + { + throw new ObjectExistsException($"A HL7 destination with the same AE Title '{item.AeTitle}', host/IP Address '{item.HostIp}' and port '{item.Port}' already exists."); + } + if (!item.IsValid(out var validationErrors)) + { + throw new ConfigurationException(string.Join(Environment.NewLine, validationErrors)); + } + } + } +} diff --git a/src/InformaticsGateway/Services/Http/HealthController.cs b/src/InformaticsGateway/Services/Http/HealthController.cs old mode 100644 new mode 100755 diff --git a/src/InformaticsGateway/Services/Http/Hl7ApplicationConfigController.cs b/src/InformaticsGateway/Services/Http/Hl7ApplicationConfigController.cs new file mode 100755 index 000000000..4eadd2f5c --- /dev/null +++ b/src/InformaticsGateway/Services/Http/Hl7ApplicationConfigController.cs @@ -0,0 +1,171 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Database.Api; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Monai.Deploy.InformaticsGateway.Logging; +using Monai.Deploy.InformaticsGateway.Services.DicomWeb; + +namespace Monai.Deploy.InformaticsGateway.Services.Http +{ + [ApiController] + [Route("configEntity/hl7-application")] + public class Hl7ApplicationConfigController : ApiControllerBase + { + private const string Endpoint = "configEntity/hl7-application"; + + private readonly ILogger _logger; + private readonly IHl7ApplicationConfigRepository _repository; + + public Hl7ApplicationConfigController(ILogger logger, + IHl7ApplicationConfigRepository repository) + { + _logger = logger; + _repository = repository; + } + + [HttpGet] + [Produces(ContentTypes.ApplicationJson)] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task Get() + { + var data = await _repository.GetAllAsync().ConfigureAwait(false); + return Ok(data); + } + + [HttpGet("{id}")] + [Produces(ContentTypes.ApplicationJson)] + [ProducesResponseType(typeof(Hl7ApplicationConfigEntity), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Get(string id) + { + var data = await _repository.GetByIdAsync(id).ConfigureAwait(false); + if (data == null) + { + return NotFound(); + } + + return Ok(data); + } + + [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task Delete(string id) + { + var data = await _repository.GetByIdAsync(id).ConfigureAwait(false); + if (data == null) + { + return NotFound(); + } + + try + { + var result = await _repository.DeleteAsync(id).ConfigureAwait(false); + return Ok(result); + } + catch (DatabaseException ex) + { + return Problem(title: "Database error removing HL7 Application Configuration.", statusCode: BadRequest, + detail: ex.Message); + } + catch (Exception ex) + { + return Problem(title: "Unknown error removing HL7 Application Configuration.", + statusCode: InternalServerError, detail: ex.Message); + } + } + + [HttpPut] + [Consumes(ContentTypes.ApplicationJson)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task Put([FromBody] Hl7ApplicationConfigEntity configEntity) + { + if (configEntity == null) + { + return Problem(title: "Hl7ApplicationConfigEntity is null.", statusCode: BadRequest, detail: "Hl7ApplicationConfigEntity is null."); + } + + var errorMessages = configEntity.Validate().ToList(); + if (errorMessages.Any()) + { + var message = "Hl7ApplicationConfigEntity is invalid. " + string.Join(", ", errorMessages); + return Problem(title: "Validation Failure.", statusCode: BadRequest, detail: message); + } + + try + { + await _repository.UpdateAsync(configEntity).ConfigureAwait(false); + return Ok(configEntity.Id); + } + catch (Exception ex) + { + _logger.PutHl7ApplicationConfigException(Endpoint, ex); + return Problem(title: "Error adding new HL7 Application Configuration.", + statusCode: InternalServerError, detail: ex.Message); + } + } + + [HttpPost] + [Consumes(ContentTypes.ApplicationJson)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task Post([FromBody] Hl7ApplicationConfigEntity configEntity) + { + if (configEntity == null) + { + return Problem(title: "Hl7ApplicationConfigEntity is null.", statusCode: BadRequest, detail: "Hl7ApplicationConfigEntity is null."); + } + + var errorMessages = configEntity.Validate().ToList(); + if (errorMessages.Any()) + { + var message = "Hl7ApplicationConfigEntity is invalid. " + string.Join(", ", errorMessages); + return Problem(title: "Validation Failure.", statusCode: BadRequest, detail: message); + } + + try + { + var result = await _repository.CreateAsync(configEntity).ConfigureAwait(false); + if (result is null) + { + return Problem(title: "Database error updating HL7 Application Configuration.", statusCode: BadRequest, + detail: "configEntity not found."); + } + return Ok(result); + } + catch (DatabaseException ex) + { + return Problem(title: "Database error updating HL7 Application Configuration.", statusCode: BadRequest, + detail: ex.Message); + } + catch (Exception ex) + { + return Problem(title: "Unknown error updating HL7 Application Configuration.", + statusCode: InternalServerError, detail: ex.Message); + } + } + } +} diff --git a/src/InformaticsGateway/Services/Http/MonaiAeTitleController.cs b/src/InformaticsGateway/Services/Http/MonaiAeTitleController.cs index 22810f4a3..fd967c01a 100755 --- a/src/InformaticsGateway/Services/Http/MonaiAeTitleController.cs +++ b/src/InformaticsGateway/Services/Http/MonaiAeTitleController.cs @@ -24,6 +24,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; diff --git a/src/InformaticsGateway/Services/Http/Startup.cs b/src/InformaticsGateway/Services/Http/Startup.cs index dfc6dc5e1..6f0779ebd 100755 --- a/src/InformaticsGateway/Services/Http/Startup.cs +++ b/src/InformaticsGateway/Services/Http/Startup.cs @@ -44,10 +44,8 @@ public Startup(IConfiguration configuration) public IConfiguration Configuration { get; } -#pragma warning disable CA1822 // Mark members as static public void ConfigureServices(IServiceCollection services) -#pragma warning restore CA1822 // Mark members as static { services.AddHttpContextAccessor(); services.AddControllers(opts => diff --git a/src/InformaticsGateway/Services/Scp/ApplicationEntityHandler.cs b/src/InformaticsGateway/Services/Scp/ApplicationEntityHandler.cs index a858a84d1..4946bc6ac 100755 --- a/src/InformaticsGateway/Services/Scp/ApplicationEntityHandler.cs +++ b/src/InformaticsGateway/Services/Scp/ApplicationEntityHandler.cs @@ -24,12 +24,14 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; using Monai.Deploy.InformaticsGateway.Logging; +using Monai.Deploy.InformaticsGateway.Services.Common; using Monai.Deploy.InformaticsGateway.Services.Connectors; using Monai.Deploy.InformaticsGateway.Services.Storage; using Monai.Deploy.Messaging.Common; @@ -48,6 +50,7 @@ internal class ApplicationEntityHandler : IDisposable, IApplicationEntityHandler private readonly IObjectUploadQueue _uploadQueue; private readonly IFileSystem _fileSystem; private readonly IInputDataPlugInEngine _pluginEngine; + private readonly IExternalAppDetailsRepository _externalAppDetailsRepository; private MonaiApplicationEntity? _configuration; private DicomJsonOptions _dicomJsonOptions; private bool _validateDicomValueOnJsonSerialization; @@ -56,7 +59,8 @@ internal class ApplicationEntityHandler : IDisposable, IApplicationEntityHandler public ApplicationEntityHandler( IServiceScopeFactory serviceScopeFactory, ILogger logger, - IOptions options) + IOptions options, + IExternalAppDetailsRepository externalAppDetailsRepository) { Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -67,6 +71,7 @@ public ApplicationEntityHandler( _uploadQueue = _serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IObjectUploadQueue)); _fileSystem = _serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IFileSystem)); _pluginEngine = _serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IInputDataPlugInEngine)); + _externalAppDetailsRepository = externalAppDetailsRepository ?? throw new ServiceNotFoundException(nameof(externalAppDetailsRepository)); } public void Configure(MonaiApplicationEntity monaiApplicationEntity, DicomJsonOptions dicomJsonOptions, bool validateDicomValuesOnJsonSerialization) @@ -89,7 +94,7 @@ public void Configure(MonaiApplicationEntity monaiApplicationEntity, DicomJsonOp } } - public async Task HandleInstanceAsync(DicomCStoreRequest request, string calledAeTitle, string callingAeTitle, Guid associationId, StudySerieSopUids uids) + public async Task HandleInstanceAsync(DicomCStoreRequest request, string calledAeTitle, string callingAeTitle, Guid associationId, StudySerieSopUids uids, ScpInputTypeEnum type) { if (_configuration is null) { @@ -107,8 +112,21 @@ public async Task HandleInstanceAsync(DicomCStoreRequest request, string _logger.InstanceIgnoredWIthMatchingSopClassUid(request.SOPClassUID.UID); return string.Empty; } + ExternalAppDetails? storedDetails; + + + + var dicomInfo = new DicomFileStorageMetadata( + associationId.ToString(), + uids.Identifier, + uids.StudyInstanceUid, + uids.SeriesInstanceUid, + uids.SopInstanceUid, + DataService.DIMSE, + callingAeTitle, + calledAeTitle); + - var dicomInfo = new DicomFileStorageMetadata(associationId.ToString(), uids.Identifier, uids.StudyInstanceUid, uids.SeriesInstanceUid, uids.SopInstanceUid, DataService.DIMSE, callingAeTitle, calledAeTitle); if (_configuration.Workflows.Any()) { @@ -128,6 +146,24 @@ public async Task HandleInstanceAsync(DicomCStoreRequest request, string using var scope = _logger.BeginScope(new Api.LoggingDataDictionary() { { "CorrelationId", dicomInfo.CorrelationId } }); + + if (type == ScpInputTypeEnum.ExternalAppReturn) + { + var uid = request.Dataset.GetSingleValue(DicomTag.StudyInstanceUID); + storedDetails = await _externalAppDetailsRepository + .GetByStudyIdOutboundAsync(uid, new System.Threading.CancellationToken()) + .ConfigureAwait(false); + if (storedDetails is null) + { + _logger.FailedToFindStoredExtAppDetails(uid); + } + RetrieveExternalAppDetails(storedDetails, dicomInfo); + dicomInfo.SetupGivenFilePaths(storedDetails?.DestinationFolder); + request.Dataset.AddOrUpdate(DicomTag.StudyInstanceUID, storedDetails?.StudyInstanceUid); + request.Dataset.AddOrUpdate(DicomTag.PatientID, storedDetails?.PatientId); + } + + dicomInfo = (result.Item2 as DicomFileStorageMetadata)!; var dicomFile = result.Item1; await dicomInfo.SetDataStreams(dicomFile, dicomFile.ToJson(_dicomJsonOptions, _validateDicomValueOnJsonSerialization), _options.Value.Storage.TemporaryDataStorage, _fileSystem, _options.Value.Storage.LocalTemporaryStoragePath).ConfigureAwait(false); @@ -143,6 +179,19 @@ public async Task HandleInstanceAsync(DicomCStoreRequest request, string return dicomInfo.PayloadId; } + private void RetrieveExternalAppDetails(ExternalAppDetails? storedDetails, DicomFileStorageMetadata dicomInfo) + { + if (storedDetails is null) + { + return; + } + dicomInfo.ChangeCorrelationId(_logger, storedDetails.CorrelationId); + dicomInfo.WorkflowInstanceId = storedDetails.WorkflowInstanceId; + dicomInfo.TaskId = storedDetails.ExportTaskID; + dicomInfo.SetStudyInstanceUid(storedDetails.StudyInstanceUid); + //dicomInfo.DestinationFolderNeil = storedDetails.DestinationFolder!; + } + private bool AcceptsSopClass(string sopClassUid) { Guard.Against.NullOrWhiteSpace(sopClassUid, nameof(sopClassUid)); diff --git a/src/InformaticsGateway/Services/Scp/ApplicationEntityManager.cs b/src/InformaticsGateway/Services/Scp/ApplicationEntityManager.cs index 00287b529..4e6aaf951 100755 --- a/src/InformaticsGateway/Services/Scp/ApplicationEntityManager.cs +++ b/src/InformaticsGateway/Services/Scp/ApplicationEntityManager.cs @@ -25,10 +25,12 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; using Monai.Deploy.InformaticsGateway.Logging; +using Monai.Deploy.InformaticsGateway.Services.Common; using Monai.Deploy.InformaticsGateway.Services.Storage; namespace Monai.Deploy.InformaticsGateway.Services.Scp @@ -91,7 +93,7 @@ private void OnApplicationStopping() _unsubscriberForMonaiAeChangedNotificationService.Dispose(); } - public async Task HandleCStoreRequest(DicomCStoreRequest request, string calledAeTitle, string callingAeTitle, Guid associationId) + public async Task HandleCStoreRequest(DicomCStoreRequest request, string calledAeTitle, string callingAeTitle, Guid associationId, ScpInputTypeEnum type) { Guard.Against.Null(request, nameof(request)); @@ -107,10 +109,10 @@ public async Task HandleCStoreRequest(DicomCStoreRequest request, string throw new InsufficientStorageAvailableException($"Insufficient storage available. Available storage space: {_storageInfoProvider.AvailableFreeSpace:D}"); } - return await HandleInstance(request, calledAeTitle, callingAeTitle, associationId).ConfigureAwait(false); + return await HandleInstance(request, calledAeTitle, callingAeTitle, associationId, type).ConfigureAwait(false); } - private async Task HandleInstance(DicomCStoreRequest request, string calledAeTitle, string callingAeTitle, Guid associationId) + private async Task HandleInstance(DicomCStoreRequest request, string calledAeTitle, string callingAeTitle, Guid associationId, ScpInputTypeEnum type) { var uids = _dicomToolkit.GetStudySeriesSopInstanceUids(request.File); @@ -124,7 +126,7 @@ private async Task HandleInstance(DicomCStoreRequest request, string cal { _logger.InstanceInformation(uids.StudyInstanceUid, uids.SeriesInstanceUid); - return await _aeTitles[calledAeTitle].HandleInstanceAsync(request, calledAeTitle, callingAeTitle, associationId, uids).ConfigureAwait(false); + return await _aeTitles[calledAeTitle].HandleInstanceAsync(request, calledAeTitle, callingAeTitle, associationId, uids, type).ConfigureAwait(false); } } diff --git a/src/InformaticsGateway/Services/Scp/ExternalAppScpService.cs b/src/InformaticsGateway/Services/Scp/ExternalAppScpService.cs new file mode 100755 index 000000000..aa6f1e459 --- /dev/null +++ b/src/InformaticsGateway/Services/Scp/ExternalAppScpService.cs @@ -0,0 +1,94 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 Ardalis.GuardClauses; +using FellowOakDicom.Network; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api.Rest; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Logging; + + +namespace Monai.Deploy.InformaticsGateway.Services.Scp +{ + internal class ExternalAppScpService : ScpServiceBase + { + private readonly IServiceScope _serviceScope; + private readonly ILogger _logger; + private readonly ILogger _scpServiceInternalLogger; + private readonly IOptions _configuration; + private readonly IApplicationEntityManager _associationDataProvider; + + public override string ServiceName => "External App DICOM SCP Service"; + + public ExternalAppScpService(IServiceScopeFactory serviceScopeFactory, + IApplicationEntityManager applicationEntityManager, + IHostApplicationLifetime appLifetime, + IOptions configuration) : base(serviceScopeFactory, applicationEntityManager, appLifetime, configuration) + { + Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); + Guard.Against.Null(applicationEntityManager, nameof(applicationEntityManager)); + Guard.Against.Null(appLifetime, nameof(appLifetime)); + Guard.Against.Null(configuration, nameof(configuration)); + + _associationDataProvider = applicationEntityManager; + + _serviceScope = serviceScopeFactory.CreateScope(); + var logginFactory = _serviceScope.ServiceProvider.GetService(); + + _logger = logginFactory!.CreateLogger(); + _scpServiceInternalLogger = logginFactory!.CreateLogger(); + _configuration = configuration; + } + + public override void ServiceStart() + { + var ScpPort = _configuration.Value.Dicom.Scp.ExternalAppPort; + try + { + _logger.AddingScpListener(ServiceName, ScpPort); + Server = DicomServerFactory.Create( + NetworkManager.IPv4Any, + ScpPort, + logger: _scpServiceInternalLogger, + userState: _associationDataProvider); + + Server.Options.IgnoreUnsupportedTransferSyntaxChange = true; + Server.Options.LogDimseDatasets = _configuration.Value.Dicom.Scp.LogDimseDatasets; + Server.Options.MaxClientsAllowed = _configuration.Value.Dicom.Scp.MaximumNumberOfAssociations; + + if (Server.Exception != null) + { + _logger.ScpListenerInitializationFailure(); + throw Server.Exception; + } + + Status = ServiceStatus.Running; + _logger.ScpListeningOnPort(ServiceName, ScpPort); + } + catch (System.Exception ex) + { + Status = ServiceStatus.Cancelled; + _logger.ServiceFailedToStart(ServiceName, ex); + AppLifetime.StopApplication(); + } + + } + } +} diff --git a/src/InformaticsGateway/Services/Scp/ExternalAppScpServiceInternal.cs b/src/InformaticsGateway/Services/Scp/ExternalAppScpServiceInternal.cs new file mode 100755 index 000000000..8a41f2c54 --- /dev/null +++ b/src/InformaticsGateway/Services/Scp/ExternalAppScpServiceInternal.cs @@ -0,0 +1,71 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * Copyright 2019-2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.Text; +using System.Threading.Tasks; +using FellowOakDicom.Network; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api.Models; +using Monai.Deploy.InformaticsGateway.Common; +using Monai.Deploy.InformaticsGateway.Logging; + + +namespace Monai.Deploy.InformaticsGateway.Services.Scp +{ + internal class ExternalAppScpServiceInternal : ScpServiceInternalBase + { + + private readonly DicomAssociationInfo _associationInfo; + private readonly ILogger _logger; + + public ExternalAppScpServiceInternal(INetworkStream stream, Encoding fallbackEncoding, ILogger logger, DicomServiceDependencies dicomServiceDependencies) + : base(stream, fallbackEncoding, logger, dicomServiceDependencies) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _associationInfo = new DicomAssociationInfo(); + } + public override async Task OnCStoreRequestAsync(DicomCStoreRequest request) + { + try + { + _logger?.TransferSyntaxUsed(request.TransferSyntax); + var payloadId = await AssociationDataProvider!.HandleCStoreRequest(request, Association.CalledAE, Association.CallingAE, AssociationId, Common.ScpInputTypeEnum.ExternalAppReturn).ConfigureAwait(false); + _associationInfo.FileReceived(payloadId); + return new DicomCStoreResponse(request, DicomStatus.Success); + } + catch (InsufficientStorageAvailableException ex) + { + _logger?.CStoreFailedDueToLowStorageSpace(ex); + _associationInfo.Errors = $"Failed to store file due to low disk space: {ex}"; + return new DicomCStoreResponse(request, DicomStatus.ResourceLimitation); + } + catch (System.IO.IOException ex) when ((ex.HResult & 0xFFFF) == Constants.ERROR_HANDLE_DISK_FULL || (ex.HResult & 0xFFFF) == Constants.ERROR_DISK_FULL) + { + _logger?.CStoreFailedWithNoSpace(ex); + _associationInfo.Errors = $"Failed to store file due to low disk space: {ex}"; + return new DicomCStoreResponse(request, DicomStatus.StorageStorageOutOfResources); + } + catch (Exception ex) + { + _logger?.CStoreFailed(ex); + _associationInfo.Errors = $"Failed to store file: {ex}"; + return new DicomCStoreResponse(request, DicomStatus.ProcessingFailure); + } + } + } +} diff --git a/src/InformaticsGateway/Services/Scp/IApplicationEntityHandler.cs b/src/InformaticsGateway/Services/Scp/IApplicationEntityHandler.cs old mode 100644 new mode 100755 index 0d3c5ed2d..f82e2d08e --- a/src/InformaticsGateway/Services/Scp/IApplicationEntityHandler.cs +++ b/src/InformaticsGateway/Services/Scp/IApplicationEntityHandler.cs @@ -17,9 +17,10 @@ using System; using System.Threading.Tasks; using FellowOakDicom.Network; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Services.Common; namespace Monai.Deploy.InformaticsGateway.Services.Scp { @@ -27,6 +28,6 @@ internal interface IApplicationEntityHandler { void Configure(MonaiApplicationEntity monaiApplicationEntity, DicomJsonOptions dicomJsonOptions, bool validateDicomValuesOnJsonSerialization); - Task HandleInstanceAsync(DicomCStoreRequest request, string calledAeTitle, string callingAeTitle, Guid associationId, StudySerieSopUids uids); + Task HandleInstanceAsync(DicomCStoreRequest request, string calledAeTitle, string callingAeTitle, Guid associationId, StudySerieSopUids uids, ScpInputTypeEnum type); } } diff --git a/src/InformaticsGateway/Services/Scp/IApplicationEntityManager.cs b/src/InformaticsGateway/Services/Scp/IApplicationEntityManager.cs old mode 100644 new mode 100755 index 7c42996c5..d8c224a04 --- a/src/InformaticsGateway/Services/Scp/IApplicationEntityManager.cs +++ b/src/InformaticsGateway/Services/Scp/IApplicationEntityManager.cs @@ -20,6 +20,7 @@ using FellowOakDicom.Network; using Microsoft.Extensions.Options; using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Services.Common; namespace Monai.Deploy.InformaticsGateway.Services.Scp { @@ -35,7 +36,7 @@ public interface IApplicationEntityManager /// Called AE Title to be associated with the call. /// Calling AE Title to be associated with the call. /// Unique association ID. - Task HandleCStoreRequest(DicomCStoreRequest request, string calledAeTitle, string callingAeTitle, Guid associationId); + Task HandleCStoreRequest(DicomCStoreRequest request, string calledAeTitle, string callingAeTitle, Guid associationId, ScpInputTypeEnum type = ScpInputTypeEnum.WorkflowTrigger); /// /// Checks if a MONAI AET is configured. diff --git a/src/InformaticsGateway/Services/Scp/IMonaiAeChangedNotificationService.cs b/src/InformaticsGateway/Services/Scp/IMonaiAeChangedNotificationService.cs old mode 100644 new mode 100755 index 2de4fccb8..adf544c61 --- a/src/InformaticsGateway/Services/Scp/IMonaiAeChangedNotificationService.cs +++ b/src/InformaticsGateway/Services/Scp/IMonaiAeChangedNotificationService.cs @@ -16,7 +16,7 @@ */ using System; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; namespace Monai.Deploy.InformaticsGateway.Services.Scp { diff --git a/src/InformaticsGateway/Services/Scp/ScpService.cs b/src/InformaticsGateway/Services/Scp/ScpService.cs old mode 100644 new mode 100755 index aae6f5db6..85c755b4a --- a/src/InformaticsGateway/Services/Scp/ScpService.cs +++ b/src/InformaticsGateway/Services/Scp/ScpService.cs @@ -1,6 +1,5 @@ -/* - * Copyright 2021-2022 MONAI Consortium - * Copyright 2019-2021 NVIDIA Corporation +/* + * Copyright 2021-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,45 +14,28 @@ * limitations under the License. */ -using System; -using System.Threading; -using System.Threading.Tasks; using Ardalis.GuardClauses; -using FellowOakDicom; -using FellowOakDicom.Network; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api.Rest; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Logging; -using Monai.Deploy.InformaticsGateway.Services.Common; - -using FoDicomNetwork = FellowOakDicom.Network; namespace Monai.Deploy.InformaticsGateway.Services.Scp { - internal sealed class ScpService : IHostedService, IDisposable, IMonaiService + internal class ScpService : ScpServiceBase { -#pragma warning disable S2223 // Non-constant static fields should not be visible - public static int ActiveConnections = 0; -#pragma warning restore S2223 // Non-constant static fields should not be visible - private readonly IServiceScope _serviceScope; - private readonly IApplicationEntityManager _associationDataProvider; private readonly ILogger _logger; - private readonly ILogger _scpServiceInternalLogger; - private readonly IHostApplicationLifetime _appLifetime; private readonly IOptions _configuration; - private FoDicomNetwork.IDicomServer? _server; - public ServiceStatus Status { get; set; } = ServiceStatus.Unknown; - public string ServiceName => "DICOM SCP Service"; + + public override string ServiceName => "DICOM SCP Service"; public ScpService(IServiceScopeFactory serviceScopeFactory, IApplicationEntityManager applicationEntityManager, IHostApplicationLifetime appLifetime, - IOptions configuration) + IOptions configuration) : base(serviceScopeFactory, applicationEntityManager, appLifetime, configuration) { Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); Guard.Against.Null(applicationEntityManager, nameof(applicationEntityManager)); @@ -61,66 +43,16 @@ public ScpService(IServiceScopeFactory serviceScopeFactory, Guard.Against.Null(configuration, nameof(configuration)); _serviceScope = serviceScopeFactory.CreateScope(); - _associationDataProvider = applicationEntityManager; - var logginFactory = _serviceScope.ServiceProvider.GetService(); _logger = logginFactory!.CreateLogger(); - _scpServiceInternalLogger = logginFactory!.CreateLogger(); - _appLifetime = appLifetime; _configuration = configuration; - _ = DicomDictionary.Default; - } - - public void Dispose() - { - _serviceScope.Dispose(); - _server?.Dispose(); - GC.SuppressFinalize(this); - } - - public Task StartAsync(CancellationToken cancellationToken) - { - _logger.ScpServiceLoading(_configuration.Value.Dicom.Scp.Port); - - try - { - _logger.ServiceStarting(ServiceName); - _server = DicomServerFactory.Create( - NetworkManager.IPv4Any, - _configuration.Value.Dicom.Scp.Port, - logger: _scpServiceInternalLogger, - userState: _associationDataProvider); - - _server.Options.IgnoreUnsupportedTransferSyntaxChange = true; - _server.Options.LogDimseDatasets = _configuration.Value.Dicom.Scp.LogDimseDatasets; - _server.Options.MaxClientsAllowed = _configuration.Value.Dicom.Scp.MaximumNumberOfAssociations; - - if (_server.Exception != null) - { - _logger.ScpListenerInitializationFailure(); - throw _server.Exception; - } - - Status = ServiceStatus.Running; - _logger.ScpListeningOnPort(_configuration.Value.Dicom.Scp.Port); - } - catch (System.Exception ex) - { - Status = ServiceStatus.Cancelled; - _logger.ServiceFailedToStart(ServiceName, ex); - _appLifetime.StopApplication(); - } - return Task.CompletedTask; } - public Task StopAsync(CancellationToken cancellationToken) + public override void ServiceStart() { - _logger.ServiceStopping(ServiceName); - _server?.Stop(); - _server?.Dispose(); - Status = ServiceStatus.Stopped; - return Task.CompletedTask; + _logger.AddingScpListener(ServiceName, _configuration.Value.Dicom.Scp.Port); + ServiceStartBase(_configuration.Value.Dicom.Scp.Port); } } } diff --git a/src/InformaticsGateway/Services/Scp/ScpServiceBase.cs b/src/InformaticsGateway/Services/Scp/ScpServiceBase.cs new file mode 100755 index 000000000..832ebec13 --- /dev/null +++ b/src/InformaticsGateway/Services/Scp/ScpServiceBase.cs @@ -0,0 +1,131 @@ +/* + * Copyright 2021-2022 MONAI Consortium + * Copyright 2019-2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.Threading; +using System.Threading.Tasks; +using Ardalis.GuardClauses; +using FellowOakDicom; +using FellowOakDicom.Network; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api.Rest; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Logging; +using Monai.Deploy.InformaticsGateway.Services.Common; + +using FoDicomNetwork = FellowOakDicom.Network; + +namespace Monai.Deploy.InformaticsGateway.Services.Scp +{ + internal abstract class ScpServiceBase : IHostedService, IDisposable, IMonaiService + { +#pragma warning disable S2223 // Non-constant static fields should not be visible + public static int ActiveConnections = 0; +#pragma warning restore S2223 // Non-constant static fields should not be visible + + private readonly IServiceScope _serviceScope; + private readonly IApplicationEntityManager _associationDataProvider; + private readonly ILogger _logger; + private readonly ILogger _scpServiceInternalLogger; + protected readonly IHostApplicationLifetime AppLifetime; + private readonly IOptions _configuration; + protected FoDicomNetwork.IDicomServer? Server; + public ServiceStatus Status { get; set; } = ServiceStatus.Unknown; + public abstract string ServiceName { get; } + + public ScpServiceBase(IServiceScopeFactory serviceScopeFactory, + IApplicationEntityManager applicationEntityManager, + IHostApplicationLifetime appLifetime, + IOptions configuration) + { + Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); + Guard.Against.Null(applicationEntityManager, nameof(applicationEntityManager)); + Guard.Against.Null(appLifetime, nameof(appLifetime)); + Guard.Against.Null(configuration, nameof(configuration)); + + _serviceScope = serviceScopeFactory.CreateScope(); + + var logginFactory = _serviceScope.ServiceProvider.GetService(); + + _logger = logginFactory!.CreateLogger(); + _scpServiceInternalLogger = logginFactory!.CreateLogger(); + _associationDataProvider = applicationEntityManager; + AppLifetime = appLifetime; + _configuration = configuration; + _ = DicomDictionary.Default; + } + + public void Dispose() + { + _serviceScope.Dispose(); + Server?.Dispose(); + GC.SuppressFinalize(this); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + ServiceStart(); + return Task.CompletedTask; + } + + public abstract void ServiceStart(); + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.ServiceStopping(ServiceName); + Server?.Stop(); + Server?.Dispose(); + Status = ServiceStatus.Stopped; + return Task.CompletedTask; + } + + public void ServiceStartBase(int ScpPort) + { + try + { + _logger.ServiceStarting(ServiceName); + Server = DicomServerFactory.Create( + NetworkManager.IPv4Any, + ScpPort, + logger: _scpServiceInternalLogger, + userState: _associationDataProvider); + + Server.Options.IgnoreUnsupportedTransferSyntaxChange = true; + Server.Options.LogDimseDatasets = _configuration.Value.Dicom.Scp.LogDimseDatasets; + Server.Options.MaxClientsAllowed = _configuration.Value.Dicom.Scp.MaximumNumberOfAssociations; + + if (Server.Exception != null) + { + _logger.ScpListenerInitializationFailure(); + throw Server.Exception; + } + + Status = ServiceStatus.Running; + _logger.ScpListeningOnPort(ServiceName, ScpPort); + } + catch (System.Exception ex) + { + Status = ServiceStatus.Cancelled; + _logger.ServiceFailedToStart(ServiceName, ex); + AppLifetime.StopApplication(); + } + } + } +} diff --git a/src/InformaticsGateway/Services/Scp/ScpServiceInternal.cs b/src/InformaticsGateway/Services/Scp/ScpServiceInternal.cs index 0ee55a106..a19ef3825 100755 --- a/src/InformaticsGateway/Services/Scp/ScpServiceInternal.cs +++ b/src/InformaticsGateway/Services/Scp/ScpServiceInternal.cs @@ -1,5 +1,5 @@ -/* - * Copyright 2021-2022 MONAI Consortium +/* + * Copyright 2021-2023 MONAI Consortium * Copyright 2019-2021 NVIDIA Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,35 +16,26 @@ */ using System; -using System.Linq; using System.Text; -using System.Threading; using System.Threading.Tasks; -using FellowOakDicom; using FellowOakDicom.Network; using Microsoft.Extensions.Logging; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Common; -using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; using Monai.Deploy.InformaticsGateway.Logging; namespace Monai.Deploy.InformaticsGateway.Services.Scp { - /// - /// A new instance of ScpServiceInternal is created for every new association. - /// - internal class ScpServiceInternal : - DicomService, - IDicomServiceProvider, - IDicomCEchoProvider, - IDicomCStoreProvider + internal class ScpServiceInternal : ScpServiceInternalBase { + + private readonly DicomAssociationInfo _associationInfo; private readonly ILogger _logger; - private IApplicationEntityManager? _associationDataProvider; - private IDisposable? _loggerScope; - private Guid _associationId; - private DateTimeOffset? _associationReceived; + //private IApplicationEntityManager? _associationDataProvider; + //private IDisposable? _loggerScope; + //private Guid _associationId; + //private DateTimeOffset? _associationReceived; public ScpServiceInternal(INetworkStream stream, Encoding fallbackEncoding, ILogger logger, DicomServiceDependencies dicomServiceDependencies) : base(stream, fallbackEncoding, logger, dicomServiceDependencies) @@ -52,42 +43,12 @@ public ScpServiceInternal(INetworkStream stream, Encoding fallbackEncoding, ILog _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _associationInfo = new DicomAssociationInfo(); } - - public Task OnCEchoRequestAsync(DicomCEchoRequest request) - { - _logger?.CEchoReceived(); - return Task.FromResult(new DicomCEchoResponse(request, DicomStatus.Success)); - } - - public void OnConnectionClosed(Exception exception) - { - if (exception != null) - { - _logger?.ConnectionClosedWithException(exception); - } - - _loggerScope?.Dispose(); - Interlocked.Decrement(ref ScpService.ActiveConnections); - - try - { - var repo = _associationDataProvider!.GetService(); - _associationInfo.Disconnect(); - repo?.AddAsync(_associationInfo).Wait(); - _logger?.ConnectionClosed(_associationInfo.CorrelationId, _associationInfo.CallingAeTitle, _associationInfo.CalledAeTitle, _associationInfo.Duration.TotalSeconds); - } - catch (Exception ex) - { - _logger?.ErrorSavingDicomAssociationInfo(_associationId, ex); - } - } - - public async Task OnCStoreRequestAsync(DicomCStoreRequest request) + public override async Task OnCStoreRequestAsync(DicomCStoreRequest request) { try { _logger?.TransferSyntaxUsed(request.TransferSyntax); - var payloadId = await _associationDataProvider!.HandleCStoreRequest(request, Association.CalledAE, Association.CallingAE, _associationId).ConfigureAwait(false); + var payloadId = await AssociationDataProvider!.HandleCStoreRequest(request, Association.CalledAE, Association.CallingAE, AssociationId, Common.ScpInputTypeEnum.WorkflowTrigger).ConfigureAwait(false); _associationInfo.FileReceived(payloadId); return new DicomCStoreResponse(request, DicomStatus.Success); } @@ -111,123 +72,5 @@ public async Task OnCStoreRequestAsync(DicomCStoreRequest r } } - public Task OnCStoreRequestExceptionAsync(string tempFileName, Exception e) - { - _logger?.CStoreFailed(e); - _associationInfo.Errors = $"Failed to store file: {e}"; - return Task.CompletedTask; - } - - public void OnReceiveAbort(DicomAbortSource source, DicomAbortReason reason) - { - _logger?.CStoreAbort(source, reason); - _associationInfo.Errors = $"{source} - {reason}"; - } - - /// - /// Start timer only if a receive association release request is received. - /// - /// - public Task OnReceiveAssociationReleaseRequestAsync() - { - var associationElapsed = TimeSpan.Zero; - if (_associationReceived.HasValue) - { - associationElapsed = DateTimeOffset.UtcNow.Subtract(_associationReceived.Value); - } - - _logger?.CStoreAssociationReleaseRequest(associationElapsed); - return SendAssociationReleaseResponseAsync(); - } - - public async Task OnReceiveAssociationRequestAsync(DicomAssociation association) - { - Interlocked.Increment(ref ScpService.ActiveConnections); - _associationReceived = DateTimeOffset.UtcNow; - _associationDataProvider = (UserState as IApplicationEntityManager)!; - - if (_associationDataProvider is null) - { - _associationInfo.Errors = $"Internal error: association data provider not found."; - throw new ServiceException($"{nameof(UserState)} must be an instance of IAssociationDataProvider"); - } - - _associationId = Guid.NewGuid(); - var associationIdStr = $"#{_associationId} {association.RemoteHost}:{association.RemotePort}"; - - _loggerScope = _logger!.BeginScope(new LoggingDataDictionary { { "Association", associationIdStr } }); - _logger.CStoreAssociationReceived(association.RemoteHost, association.RemotePort); - - _associationInfo.CallingAeTitle = association.CallingAE; - _associationInfo.CalledAeTitle = association.CalledAE; - _associationInfo.RemoteHost = association.RemoteHost; - _associationInfo.RemotePort = association.RemotePort; - _associationInfo.CorrelationId = _associationId.ToString(); - - if (!await IsValidSourceAeAsync(association.CallingAE, association.RemoteHost).ConfigureAwait(false)) - { - _associationInfo.Errors = $"Invalid source. Called AE: {association.CalledAE}. Calling AE: {association.CallingAE}. IP: {association.RemoteHost}."; - - await SendAssociationRejectAsync( - DicomRejectResult.Permanent, - DicomRejectSource.ServiceUser, - DicomRejectReason.CallingAENotRecognized).ConfigureAwait(false); - } - - if (!await IsValidCalledAeAsync(association.CalledAE).ConfigureAwait(false)) - { - _associationInfo.Errors = "Invalid MONAI AE Title. Called AE: {association.CalledAE}. Calling AE: {association.CallingAE}. IP: {association.RemoteHost}."; - - await SendAssociationRejectAsync( - DicomRejectResult.Permanent, - DicomRejectSource.ServiceUser, - DicomRejectReason.CalledAENotRecognized).ConfigureAwait(false); - } - - foreach (var pc in association.PresentationContexts) - { - if (pc.AbstractSyntax == DicomUID.Verification) - { - if (!_associationDataProvider.Configuration.Value.Dicom.Scp.EnableVerification) - { - _associationInfo.Errors = "Verification service disabled. Called AE: {association.CalledAE}. Calling AE: {association.CallingAE}. IP: {association.RemoteHost}."; - _logger?.VerificationServiceDisabled(); - await SendAssociationRejectAsync( - DicomRejectResult.Permanent, - DicomRejectSource.ServiceUser, - DicomRejectReason.ApplicationContextNotSupported - ).ConfigureAwait(false); - } - pc.AcceptTransferSyntaxes(_associationDataProvider.Configuration.Value.Dicom.Scp.VerificationServiceTransferSyntaxes.ToDicomTransferSyntaxArray()); - } - else if (pc.AbstractSyntax.StorageCategory != DicomStorageCategory.None) - { - if (!_associationDataProvider.CanStore) - { - _associationInfo.Errors = "Disk pressure. Called AE: {association.CalledAE}. Calling AE: {association.CallingAE}. IP: {association.RemoteHost}."; - await SendAssociationRejectAsync( - DicomRejectResult.Permanent, - DicomRejectSource.ServiceUser, - DicomRejectReason.NoReasonGiven).ConfigureAwait(false); - } - // Accept any proposed TS - pc.AcceptTransferSyntaxes(pc.GetTransferSyntaxes().ToArray()); - } - } - - await SendAssociationAcceptAsync(association).ConfigureAwait(false); - } - - private async Task IsValidCalledAeAsync(string calledAe) - { - return await _associationDataProvider!.IsAeTitleConfiguredAsync(calledAe).ConfigureAwait(false); - } - - private async Task IsValidSourceAeAsync(string callingAe, string host) - { - if (!_associationDataProvider!.Configuration.Value.Dicom.Scp.RejectUnknownSources) return true; - - return await _associationDataProvider.IsValidSourceAsync(callingAe, host).ConfigureAwait(false); - } } } diff --git a/src/InformaticsGateway/Services/Scp/ScpServiceInternalBase.cs b/src/InformaticsGateway/Services/Scp/ScpServiceInternalBase.cs new file mode 100755 index 000000000..3548aaaf4 --- /dev/null +++ b/src/InformaticsGateway/Services/Scp/ScpServiceInternalBase.cs @@ -0,0 +1,207 @@ +/* + * Copyright 2021-2022 MONAI Consortium + * Copyright 2019-2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using FellowOakDicom; +using FellowOakDicom.Network; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; +using Monai.Deploy.InformaticsGateway.Common; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Monai.Deploy.InformaticsGateway.Logging; + +namespace Monai.Deploy.InformaticsGateway.Services.Scp +{ + /// + /// A new instance of ScpServiceInternal is created for every new association. + /// + internal abstract class ScpServiceInternalBase : + DicomService, + IDicomServiceProvider, + IDicomCEchoProvider, + IDicomCStoreProvider + { + private readonly DicomAssociationInfo _associationInfo; + private readonly ILogger _logger; + protected IApplicationEntityManager? AssociationDataProvider; + private IDisposable? _loggerScope; + protected Guid AssociationId; + private DateTimeOffset? _associationReceived; + + public ScpServiceInternalBase(INetworkStream stream, Encoding fallbackEncoding, ILogger logger, DicomServiceDependencies dicomServiceDependencies) + : base(stream, fallbackEncoding, logger, dicomServiceDependencies) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _associationInfo = new DicomAssociationInfo(); + } + + public Task OnCEchoRequestAsync(DicomCEchoRequest request) + { + _logger?.CEchoReceived(); + return Task.FromResult(new DicomCEchoResponse(request, DicomStatus.Success)); + } + + public void OnConnectionClosed(Exception exception) + { + if (exception != null) + { + _logger?.ConnectionClosedWithException(exception); + } + + _loggerScope?.Dispose(); + Interlocked.Decrement(ref ScpServiceBase.ActiveConnections); + + try + { + var repo = AssociationDataProvider!.GetService(); + _associationInfo.Disconnect(); + repo?.AddAsync(_associationInfo).Wait(); + _logger?.ConnectionClosed(_associationInfo.CorrelationId, _associationInfo.CallingAeTitle, _associationInfo.CalledAeTitle, _associationInfo.Duration.TotalSeconds); + } + catch (Exception ex) + { + _logger?.ErrorSavingDicomAssociationInfo(AssociationId, ex); + } + } + + public abstract Task OnCStoreRequestAsync(DicomCStoreRequest request); + + public Task OnCStoreRequestExceptionAsync(string tempFileName, Exception e) + { + _logger?.CStoreFailed(e); + _associationInfo.Errors = $"Failed to store file: {e}"; + return Task.CompletedTask; + } + + public void OnReceiveAbort(DicomAbortSource source, DicomAbortReason reason) + { + _logger?.CStoreAbort(source, reason); + _associationInfo.Errors = $"{source} - {reason}"; + } + + /// + /// Start timer only if a receive association release request is received. + /// + /// + public Task OnReceiveAssociationReleaseRequestAsync() + { + var associationElapsed = TimeSpan.Zero; + if (_associationReceived.HasValue) + { + associationElapsed = DateTimeOffset.UtcNow.Subtract(_associationReceived.Value); + } + + _logger?.CStoreAssociationReleaseRequest(associationElapsed); + return SendAssociationReleaseResponseAsync(); + } + + public async Task OnReceiveAssociationRequestAsync(DicomAssociation association) + { + Interlocked.Increment(ref ScpServiceBase.ActiveConnections); + _associationReceived = DateTimeOffset.UtcNow; + AssociationDataProvider = (UserState as IApplicationEntityManager)!; + + if (AssociationDataProvider is null) + { + _associationInfo.Errors = $"Internal error: association data provider not found."; + throw new ServiceException($"{nameof(UserState)} must be an instance of IAssociationDataProvider"); + } + + AssociationId = Guid.NewGuid(); + var associationIdStr = $"#{AssociationId} {association.RemoteHost}:{association.RemotePort}"; + + _loggerScope = _logger!.BeginScope(new LoggingDataDictionary { { "Association", associationIdStr } }); + _logger.CStoreAssociationReceived(association.RemoteHost, association.RemotePort); + + _associationInfo.CallingAeTitle = association.CallingAE; + _associationInfo.CalledAeTitle = association.CalledAE; + _associationInfo.RemoteHost = association.RemoteHost; + _associationInfo.RemotePort = association.RemotePort; + _associationInfo.CorrelationId = AssociationId.ToString(); + + if (!await IsValidSourceAeAsync(association.CallingAE, association.RemoteHost).ConfigureAwait(false)) + { + _associationInfo.Errors = $"Invalid source. Called AE: {association.CalledAE}. Calling AE: {association.CallingAE}. IP: {association.RemoteHost}."; + + await SendAssociationRejectAsync( + DicomRejectResult.Permanent, + DicomRejectSource.ServiceUser, + DicomRejectReason.CallingAENotRecognized).ConfigureAwait(false); + } + + if (!await IsValidCalledAeAsync(association.CalledAE).ConfigureAwait(false)) + { + _associationInfo.Errors = "Invalid MONAI AE Title. Called AE: {association.CalledAE}. Calling AE: {association.CallingAE}. IP: {association.RemoteHost}."; + + await SendAssociationRejectAsync( + DicomRejectResult.Permanent, + DicomRejectSource.ServiceUser, + DicomRejectReason.CalledAENotRecognized).ConfigureAwait(false); + } + + foreach (var pc in association.PresentationContexts) + { + if (pc.AbstractSyntax == DicomUID.Verification) + { + if (!AssociationDataProvider.Configuration.Value.Dicom.Scp.EnableVerification) + { + _associationInfo.Errors = "Verification service disabled. Called AE: {association.CalledAE}. Calling AE: {association.CallingAE}. IP: {association.RemoteHost}."; + _logger?.VerificationServiceDisabled(); + await SendAssociationRejectAsync( + DicomRejectResult.Permanent, + DicomRejectSource.ServiceUser, + DicomRejectReason.ApplicationContextNotSupported + ).ConfigureAwait(false); + } + pc.AcceptTransferSyntaxes(AssociationDataProvider.Configuration.Value.Dicom.Scp.VerificationServiceTransferSyntaxes.ToDicomTransferSyntaxArray()); + } + else if (pc.AbstractSyntax.StorageCategory != DicomStorageCategory.None) + { + if (!AssociationDataProvider.CanStore) + { + _associationInfo.Errors = "Disk pressure. Called AE: {association.CalledAE}. Calling AE: {association.CallingAE}. IP: {association.RemoteHost}."; + await SendAssociationRejectAsync( + DicomRejectResult.Permanent, + DicomRejectSource.ServiceUser, + DicomRejectReason.NoReasonGiven).ConfigureAwait(false); + } + // Accept any proposed TS + pc.AcceptTransferSyntaxes(pc.GetTransferSyntaxes().ToArray()); + } + } + + await SendAssociationAcceptAsync(association).ConfigureAwait(false); + } + + private async Task IsValidCalledAeAsync(string calledAe) + { + return await AssociationDataProvider!.IsAeTitleConfiguredAsync(calledAe).ConfigureAwait(false); + } + + private async Task IsValidSourceAeAsync(string callingAe, string host) + { + if (!AssociationDataProvider!.Configuration.Value.Dicom.Scp.RejectUnknownSources) return true; + + return await AssociationDataProvider.IsValidSourceAsync(callingAe, host).ConfigureAwait(false); + } + } +} diff --git a/src/InformaticsGateway/Services/Scu/ScuService.cs b/src/InformaticsGateway/Services/Scu/ScuService.cs index 809f12cfa..1833955d7 100755 --- a/src/InformaticsGateway/Services/Scu/ScuService.cs +++ b/src/InformaticsGateway/Services/Scu/ScuService.cs @@ -205,7 +205,6 @@ public Task StartAsync(CancellationToken cancellationToken) }, CancellationToken.None); Status = ServiceStatus.Running; - _logger.ServiceRunning(ServiceName); if (task.IsCompleted) return task; return Task.CompletedTask; diff --git a/src/InformaticsGateway/Services/Storage/ObjectUploadService.cs b/src/InformaticsGateway/Services/Storage/ObjectUploadService.cs index a276dca33..49d894728 100755 --- a/src/InformaticsGateway/Services/Storage/ObjectUploadService.cs +++ b/src/InformaticsGateway/Services/Storage/ObjectUploadService.cs @@ -167,7 +167,7 @@ private async Task ProcessObject(int thread, FileStorageMetadata blob) catch (Exception ex) { blob.SetFailed(); - _logger.FailedToUploadFile(blob.Id, ex); + _logger.FailedToUploadFile(blob.Id, blob.File.UploadPath, ex); } finally { diff --git a/src/InformaticsGateway/Test/Monai.Deploy.InformaticsGateway.Test.csproj b/src/InformaticsGateway/Test/Monai.Deploy.InformaticsGateway.Test.csproj index 9680120d1..7c303125e 100755 --- a/src/InformaticsGateway/Test/Monai.Deploy.InformaticsGateway.Test.csproj +++ b/src/InformaticsGateway/Test/Monai.Deploy.InformaticsGateway.Test.csproj @@ -36,8 +36,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + - + diff --git a/src/InformaticsGateway/Test/Plug-ins/TestInputHL7DataPlugs.cs b/src/InformaticsGateway/Test/Plug-ins/TestInputHL7DataPlugs.cs new file mode 100755 index 000000000..c24e93270 --- /dev/null +++ b/src/InformaticsGateway/Test/Plug-ins/TestInputHL7DataPlugs.cs @@ -0,0 +1,37 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.Reflection; +using HL7.Dotnetcore; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; +using Monai.Deploy.InformaticsGateway.Api.Storage; + +namespace Monai.Deploy.InformaticsGateway.Test.PlugIns +{ + [PlugInName("TestInputHL7DataPlugInAddWorkflow")] + public class TestInputHL7DataPlugInAddWorkflow : IInputHL7DataPlugIn + { + public static readonly string TestString = "HOSPITAL changed!"; + + public string Name => GetType().GetCustomAttribute()?.Name ?? GetType().Name; + + public Task<(Message hl7Message, FileStorageMetadata fileMetadata)> ExecuteAsync(Message hl7File, FileStorageMetadata fileMetadata) + { + hl7File.SetValue("MSH.3", TestString); + fileMetadata.Workflows.Add(TestString); + return Task.FromResult((hl7File, fileMetadata)); + } + } +} diff --git a/src/InformaticsGateway/Test/Plug-ins/TestOutputDataPlugIns.cs b/src/InformaticsGateway/Test/Plug-ins/TestOutputDataPlugIns.cs old mode 100644 new mode 100755 index 171e7c3d6..8a95ca525 --- a/src/InformaticsGateway/Test/Plug-ins/TestOutputDataPlugIns.cs +++ b/src/InformaticsGateway/Test/Plug-ins/TestOutputDataPlugIns.cs @@ -16,7 +16,7 @@ using System.Reflection; using FellowOakDicom; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Api.PlugIns; namespace Monai.Deploy.InformaticsGateway.Test.PlugIns diff --git a/src/InformaticsGateway/Test/Repositories/MonaiServiceLocatorTest.cs b/src/InformaticsGateway/Test/Repositories/MonaiServiceLocatorTest.cs old mode 100644 new mode 100755 index 012425de7..d7a3dc915 --- a/src/InformaticsGateway/Test/Repositories/MonaiServiceLocatorTest.cs +++ b/src/InformaticsGateway/Test/Repositories/MonaiServiceLocatorTest.cs @@ -49,11 +49,14 @@ public void GetMonaiServices() items => items.ServiceName.Equals("DataRetrievalService"), items => items.ServiceName.Equals("ScpService"), items => items.ServiceName.Equals("ScuService"), + items => items.ServiceName.Equals("ExtAppScuService"), items => items.ServiceName.Equals("SpaceReclaimerService"), items => items.ServiceName.Equals("DicomWebExportService"), items => items.ServiceName.Equals("ScuExportService"), items => items.ServiceName.Equals("PayloadNotificationService"), - items => items.ServiceName.Equals("HL7 Service")); + items => items.ServiceName.Equals("HL7 Service"), + items => items.ServiceName.Equals("ExtAppScuExportService"), + items => items.ServiceName.Equals("Hl7ExportService")); } [Fact(DisplayName = "GetServiceStatus")] @@ -62,7 +65,7 @@ public void GetServiceStatus() var serviceLocator = new MonaiServiceLocator(_serviceProvider.Object); var result = serviceLocator.GetServiceStatus(); - Assert.Equal(8, result.Count); + Assert.Equal(11, result.Count); foreach (var svc in result.Keys) { Assert.Equal(ServiceStatus.Running, result[svc]); diff --git a/src/InformaticsGateway/Test/Services/Common/InputHL7DataPlugInEngineFactoryTest.cs b/src/InformaticsGateway/Test/Services/Common/InputHL7DataPlugInEngineFactoryTest.cs new file mode 100755 index 000000000..47a56a735 --- /dev/null +++ b/src/InformaticsGateway/Test/Services/Common/InputHL7DataPlugInEngineFactoryTest.cs @@ -0,0 +1,69 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; +using Monai.Deploy.InformaticsGateway.Common; +using Monai.Deploy.InformaticsGateway.Services.Common; +using Monai.Deploy.InformaticsGateway.SharedTest; +using Monai.Deploy.InformaticsGateway.Test.PlugIns; +using Moq; +using Xunit; +using Xunit.Abstractions; +namespace Monai.Deploy.InformaticsGateway.Test.Services.Common +{ + public class InputHL7DataPlugInEngineFactoryTest + { + private readonly Mock> _logger; + private readonly FileSystem _fileSystem; + private readonly ITestOutputHelper _output; + + public InputHL7DataPlugInEngineFactoryTest(ITestOutputHelper output) + { + _logger = new Mock>(); + _fileSystem = new FileSystem(); + _output = output; + + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + } + + [Fact] + public void RegisteredPlugIns_WhenCalled_ReturnsListOfPlugIns() + { + var factory = new InputHL7DataPlugInEngineFactory(_fileSystem, _logger.Object); + var result = factory.RegisteredPlugIns().OrderBy(p => p.Value).ToArray(); + + _output.WriteLine($"result now = {JsonSerializer.Serialize(result)}"); + + Assert.Collection(result, + p => VerifyPlugIn(p, typeof(TestInputHL7DataPlugInAddWorkflow))); + + _logger.VerifyLogging($"{typeof(IInputHL7DataPlugIn).Name} data plug-in found {typeof(TestInputHL7DataPlugInAddWorkflow).GetCustomAttribute()?.Name}: {typeof(TestInputHL7DataPlugInAddWorkflow).GetShortTypeAssemblyName()}.", LogLevel.Information, Times.Once()); + } + + private void VerifyPlugIn(KeyValuePair values, Type type) + { + Assert.Equal(values.Key, type.GetCustomAttribute()?.Name); + Assert.Equal(values.Value, type.GetShortTypeAssemblyName()); + } + } +} diff --git a/src/InformaticsGateway/Test/Services/Common/InputHL7DataPlugInEngineTest.cs b/src/InformaticsGateway/Test/Services/Common/InputHL7DataPlugInEngineTest.cs new file mode 100755 index 000000000..4f3298a7b --- /dev/null +++ b/src/InformaticsGateway/Test/Services/Common/InputHL7DataPlugInEngineTest.cs @@ -0,0 +1,145 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Storage; +using Monai.Deploy.InformaticsGateway.Common; +using Monai.Deploy.InformaticsGateway.Services.Common; +using Monai.Deploy.InformaticsGateway.Test.PlugIns; +using Monai.Deploy.Messaging.Events; +using Moq; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.Test.Services.Common +{ + public class InputHL7DataPlugInEngineTest + { + private readonly Mock> _logger; + private readonly Mock _serviceScopeFactory; + private readonly Mock _serviceScope; + private readonly ServiceProvider _serviceProvider; + + private const string SampleMessage = "MSH|^~\\&|MD|MD HOSPITAL|MD Test|MONAI Deploy|202207130000|SECURITY|MD^A01^ADT_A01|MSG00001|P|2.8||||\r\n"; + + public InputHL7DataPlugInEngineTest() + { + _logger = new Mock>(); + _serviceScopeFactory = new Mock(); + _serviceScope = new Mock(); + + var services = new ServiceCollection(); + services.AddScoped(p => _logger.Object); + + _serviceProvider = services.BuildServiceProvider(); + _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); + _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); + + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + } + + [Fact] + public void GivenAnInputHL7DataPlugInEngine_WhenInitialized_ExpectParametersToBeValidated() + { + Assert.Throws(() => new InputHL7DataPlugInEngine(null, null)); + Assert.Throws(() => new InputHL7DataPlugInEngine(_serviceProvider, null)); + + _ = new InputHL7DataPlugInEngine(_serviceProvider, _logger.Object); + } + + + [Fact] + public void GivenAnInputHL7DataPlugInEngine_WhenConfigureIsCalledWithBogusAssemblies_ThrowsException() + { + var pluginEngine = new InputHL7DataPlugInEngine(_serviceProvider, _logger.Object); + var assemblies = new List() { "SomeBogusAssemblye" }; + + var exceptions = Assert.Throws(() => pluginEngine.Configure(assemblies)); + + Assert.Single(exceptions.InnerExceptions); + Assert.True(exceptions.InnerException is PlugInLoadingException); + Assert.Contains("Error loading plug-in 'SomeBogusAssemblye'", exceptions.InnerException.Message); + } + + [Fact] + public void GivenAnInputHL7DataPlugInEngine_WhenConfigureIsCalledWithAValidAssembly_ExpectNoExceptions() + { + var pluginEngine = new InputHL7DataPlugInEngine(_serviceProvider, _logger.Object); + var assemblies = new List() { typeof(TestInputHL7DataPlugInAddWorkflow).AssemblyQualifiedName }; + + pluginEngine.Configure(assemblies); + Assert.NotNull(pluginEngine); + } + + [Fact] + public async Task GivenAnInputHL7DataPlugInEngine_WhenExecutePlugInsIsCalledWithoutConfigure_ThrowsException() + { + var pluginEngine = new InputHL7DataPlugInEngine(_serviceProvider, _logger.Object); + var assemblies = new List() { typeof(TestInputHL7DataPlugInAddWorkflow).AssemblyQualifiedName }; + + var dicomInfo = new DicomFileStorageMetadata( + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + "StudyInstanceUID", + "SeriesInstanceUID", + "SOPInstanceUID", + DataService.DicomWeb, + "calling", + "called"); + + var message = new HL7.Dotnetcore.Message(SampleMessage); + message.ParseMessage(); + + await Assert.ThrowsAsync(async () => await pluginEngine.ExecutePlugInsAsync(message, dicomInfo, null)); + } + + [Fact] + public async Task GivenAnInputHL7DataPlugInEngine_WhenExecutePlugInsIsCalled_ExpectDataIsProcessedByPlugInAsync() + { + var pluginEngine = new InputHL7DataPlugInEngine(_serviceProvider, _logger.Object); + var assemblies = new List() + { + typeof(TestInputHL7DataPlugInAddWorkflow).AssemblyQualifiedName, + }; + + pluginEngine.Configure(assemblies); + + var dicomInfo = new DicomFileStorageMetadata( + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + "StudyInstanceUID", + "SeriesInstanceUID", + "SOPInstanceUID", + DataService.DicomWeb, + "calling", + "called"); + + var message = new HL7.Dotnetcore.Message(SampleMessage); + message.ParseMessage(); + var configItem = new Hl7ApplicationConfigEntity { PlugInAssemblies = new List { { "TestInputHL7DataPlugInAddWorkflow" } } }; + + var (Hl7Message, resultDicomInfo) = await pluginEngine.ExecutePlugInsAsync(message, dicomInfo, configItem); + + Assert.Equal(Hl7Message, message); + Assert.Equal(resultDicomInfo, dicomInfo); + Assert.Equal(Hl7Message.GetValue("MSH.3"), TestInputHL7DataPlugInAddWorkflow.TestString); + } + } +} diff --git a/src/InformaticsGateway/Test/Services/Common/OutputDataPluginEngineTest.cs b/src/InformaticsGateway/Test/Services/Common/OutputDataPluginEngineTest.cs old mode 100644 new mode 100755 index 90595f2ff..faee1ca97 --- a/src/InformaticsGateway/Test/Services/Common/OutputDataPluginEngineTest.cs +++ b/src/InformaticsGateway/Test/Services/Common/OutputDataPluginEngineTest.cs @@ -21,7 +21,7 @@ using FellowOakDicom; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Services.Common; using Monai.Deploy.InformaticsGateway.Test.PlugIns; diff --git a/src/InformaticsGateway/Test/Services/Export/DicomWebExportServiceTest.cs b/src/InformaticsGateway/Test/Services/Export/DicomWebExportServiceTest.cs index 97db11eda..83e4c2036 100755 --- a/src/InformaticsGateway/Test/Services/Export/DicomWebExportServiceTest.cs +++ b/src/InformaticsGateway/Test/Services/Export/DicomWebExportServiceTest.cs @@ -28,7 +28,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Api.Rest; using Monai.Deploy.InformaticsGateway.Common; diff --git a/src/InformaticsGateway/Test/Services/Export/ExportHl7ServiceTests.cs b/src/InformaticsGateway/Test/Services/Export/ExportHl7ServiceTests.cs new file mode 100755 index 000000000..0897a68a2 --- /dev/null +++ b/src/InformaticsGateway/Test/Services/Export/ExportHl7ServiceTests.cs @@ -0,0 +1,385 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Ardalis.GuardClauses; +using FellowOakDicom; +using FellowOakDicom.Network; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api.Mllp; +using Monai.Deploy.InformaticsGateway.Api.Models; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; +using Monai.Deploy.InformaticsGateway.Common; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Monai.Deploy.InformaticsGateway.Services.Export; +using Monai.Deploy.InformaticsGateway.Services.HealthLevel7; +using Monai.Deploy.InformaticsGateway.Services.Storage; +using Monai.Deploy.InformaticsGateway.SharedTest; +using Monai.Deploy.Messaging.API; +using Monai.Deploy.Messaging.Common; +using Monai.Deploy.Messaging.Events; +using Monai.Deploy.Messaging.Messages; +using Monai.Deploy.Storage.API; +using Moq; +using xRetry; +using Xunit; + + +namespace Monai.Deploy.InformaticsGateway.Test.Services.Export +{ + [Collection("Hl7 Export Listener")] + public class ExportHl7ServiceTests + { + private readonly Mock _storageService = new Mock(); + private readonly Mock _messageSubscriberService = new Mock(); + private readonly Mock _messagePublisherService = new Mock(); + private readonly Mock> _logger = new Mock>(); + private readonly Mock _extAppScpLogger = new Mock(); + private readonly Mock _serviceScopeFactory = new Mock(); + private readonly IOptions _configuration; + private readonly Mock _dicomToolkit = new Mock(); + private readonly Mock _storageInfoProvider = new Mock(); + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private readonly Mock _mllpService = new Mock(); + private readonly Mock _outputDataPlugInEngine = new Mock(); + private readonly Mock _repository = new Mock(); + private readonly int _port = 1104; + + public ExportHl7ServiceTests() + { + _configuration = Options.Create(new InformaticsGatewayConfiguration()); + + var services = new ServiceCollection(); + services.AddScoped(p => _messagePublisherService.Object); + services.AddScoped(p => _messageSubscriberService.Object); + services.AddScoped(p => _storageService.Object); + services.AddScoped(p => _storageInfoProvider.Object); + services.AddScoped(p => _mllpService.Object); + services.AddScoped(p => _outputDataPlugInEngine.Object); + services.AddScoped(p => _repository.Object); + + var serviceProvider = services.BuildServiceProvider(); + + var scope = new Mock(); + scope.Setup(x => x.ServiceProvider).Returns(serviceProvider); + + _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(scope.Object); + _configuration.Value.Export.Retries.DelaysMilliseconds = new[] { 1 }; + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + _storageInfoProvider.Setup(p => p.HasSpaceAvailableForExport).Returns(true); + _outputDataPlugInEngine.Setup(p => p.Configure(It.IsAny>())); + _outputDataPlugInEngine.Setup(p => p.ExecutePlugInsAsync(It.IsAny())) + .Returns((ExportRequestDataMessage message) => Task.FromResult(message)); + } + + [RetryFact(1, 250, DisplayName = "Constructor - throws on null params")] + public void Constructor_ThrowsOnNullParams() + { + Assert.Throws(() => new Hl7ExportService(null, null, null, null)); + Assert.Throws(() => new Hl7ExportService(_logger.Object, null, null, null)); + Assert.Throws(() => new Hl7ExportService(_logger.Object, _serviceScopeFactory.Object, null, null)); + Assert.Throws(() => new Hl7ExportService(_logger.Object, _serviceScopeFactory.Object, _configuration, null)); + } + + + [RetryFact(10, 250, DisplayName = "When no destination is defined")] + public async Task ShallFailWhenNoDestinationIsDefined() + { + _messagePublisherService.Setup(p => p.Publish(It.IsAny(), It.IsAny())); + _messageSubscriberService.Setup(p => p.Acknowledge(It.IsAny())); + _messageSubscriberService.Setup(p => p.RequeueWithDelay(It.IsAny())); + _messageSubscriberService.Setup( + p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Callback, ushort>(async (topic, queue, messageReceivedCallback, prefetchCount) => + { + await messageReceivedCallback(CreateMessageReceivedEventArgs(string.Empty)); + }); + + _storageService.Setup(p => p.GetObjectAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + + var service = new Hl7ExportService(_logger.Object, _serviceScopeFactory.Object, _configuration, _dicomToolkit.Object); + + var dataflowCompleted = new ManualResetEvent(false); + service.ReportActionCompleted += (sender, args) => + { + dataflowCompleted.Set(); + }; + + await service.StartAsync(_cancellationTokenSource.Token); + Assert.True(dataflowCompleted.WaitOne(3000)); + await StopAndVerify(service); + + _messagePublisherService.Verify( + p => p.Publish(It.IsAny(), + It.Is(match => CheckMessage(match, ExportStatus.Failure, FileExportStatus.ConfigurationError))), Times.Once()); + _messageSubscriberService.Verify(p => p.Acknowledge(It.IsAny()), Times.Once()); + _messageSubscriberService.Verify(p => p.RequeueWithDelay(It.IsAny()), Times.Never()); + _messageSubscriberService.Verify(p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny()), Times.Once()); + _logger.VerifyLogging("Export task does not have destination set.", LogLevel.Error, Times.Once()); + } + + [RetryFact(10, 250, DisplayName = "When destination is not configured")] + public async Task ShallFailWhenDestinationIsNotConfigured() + { + _messagePublisherService.Setup(p => p.Publish(It.IsAny(), It.IsAny())); + _messageSubscriberService.Setup(p => p.Acknowledge(It.IsAny())); + _messageSubscriberService.Setup(p => p.RequeueWithDelay(It.IsAny())); + _messageSubscriberService.Setup( + p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Callback, ushort>(async (topic, queue, messageReceivedCallback, prefetchCount) => + { + await messageReceivedCallback(CreateMessageReceivedEventArgs("pacs")); + }); + + _storageService.Setup(p => p.GetObjectAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).ReturnsAsync(default(HL7DestinationEntity)); + + var service = new Hl7ExportService(_logger.Object, _serviceScopeFactory.Object, _configuration, _dicomToolkit.Object); + + var dataflowCompleted = new ManualResetEvent(false); + service.ReportActionCompleted += (sender, args) => + { + dataflowCompleted.Set(); + }; + + await service.StartAsync(_cancellationTokenSource.Token); + Assert.True(dataflowCompleted.WaitOne(3000)); + await StopAndVerify(service); + + _messagePublisherService.Verify( + p => p.Publish(It.IsAny(), + It.Is(match => CheckMessage(match, ExportStatus.Failure, FileExportStatus.ConfigurationError))), Times.Once()); + _messageSubscriberService.Verify(p => p.Acknowledge(It.IsAny()), Times.Once()); + _messageSubscriberService.Verify(p => p.RequeueWithDelay(It.IsAny()), Times.Never()); + _messageSubscriberService.Verify(p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny()), Times.Once()); + + _logger.VerifyLogging($"Specified destination 'pacs' does not exist.", LogLevel.Error, Times.Once()); + } + + [RetryFact(1, 250, DisplayName = "HL7 message rejected")] + public async Task No_Ack_Sent() + { + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var destination = new HL7DestinationEntity { HostIp = "192.168.0.0", Port = _port }; + + _messagePublisherService.Setup(p => p.Publish(It.IsAny(), It.IsAny())); + _messageSubscriberService.Setup(p => p.Acknowledge(It.IsAny())); + _messageSubscriberService.Setup(p => p.RequeueWithDelay(It.IsAny())); + _messageSubscriberService.Setup( + p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Callback, ushort>(async (topic, queue, messageReceivedCallback, prefetchCount) => + { + await messageReceivedCallback(CreateMessageReceivedEventArgs("pacs")); + }); + + _mllpService.Setup(p => p.SendMllp(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Throws(new Hl7SendException("Send exception")); + + _storageService.Setup(p => p.GetObjectAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).ReturnsAsync(destination); + _dicomToolkit.Setup(p => p.Load(It.IsAny())).Returns(InstanceGenerator.GenerateDicomFile(sopInstanceUid: sopInstanceUid)); + + var service = new Hl7ExportService(_logger.Object, _serviceScopeFactory.Object, _configuration, _dicomToolkit.Object); + + var dataflowCompleted = new ManualResetEvent(false); + service.ReportActionCompleted += (sender, args) => + { + dataflowCompleted.Set(); + }; + + await service.StartAsync(_cancellationTokenSource.Token); + Assert.True(dataflowCompleted.WaitOne(5000)); + + await StopAndVerify(service); + _messagePublisherService.Verify( + p => p.Publish(It.IsAny(), + It.Is(match => CheckMessage(match, ExportStatus.Failure, FileExportStatus.ServiceError))), Times.Once()); + _messageSubscriberService.Verify(p => p.Acknowledge(It.IsAny()), Times.Once()); + _messageSubscriberService.Verify(p => p.RequeueWithDelay(It.IsAny()), Times.Never()); + _messageSubscriberService.Verify(p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny()), Times.Once()); + + _logger.Verify(x => x.Log( + LogLevel.Error, + 538, // this is the eventId of the log we're looking for + It.Is((v, t) => true), + It.IsAny(), + It.Is>((v, t) => true))); + } + + [RetryFact(1, 250, DisplayName = "Failed to load message content")] + public async Task Error_Loading_HL7_Content() + { + + _extAppScpLogger.Invocations.Clear(); + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var destination = new HL7DestinationEntity { HostIp = "192.168.0.0", Port = _port }; + var service = new Hl7ExportService(_logger.Object, _serviceScopeFactory.Object, _configuration, _dicomToolkit.Object); + + _messagePublisherService.Setup(p => p.Publish(It.IsAny(), It.IsAny())); + _messageSubscriberService.Setup(p => p.Acknowledge(It.IsAny())); + _messageSubscriberService.Setup(p => p.RequeueWithDelay(It.IsAny())); + _messageSubscriberService.Setup( + p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Callback, ushort>(async (topic, queue, messageReceivedCallback, prefetchCount) => + { + await messageReceivedCallback(CreateMessageReceivedEventArgs("pacs")); + }); + + _storageService.Setup(p => p.GetObjectAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(null)); + + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).ReturnsAsync(destination); + + + var dataflowCompleted = new ManualResetEvent(false); + service.ReportActionCompleted += (sender, args) => + { + dataflowCompleted.Set(); + }; + + DicomScpFixture.DicomStatus = DicomStatus.Success; + await service.StartAsync(_cancellationTokenSource.Token); + Assert.True(dataflowCompleted.WaitOne(5000)); + await StopAndVerify(service); + + _messagePublisherService.Verify( + p => p.Publish(It.IsAny(), + It.Is(match => CheckMessage(match, ExportStatus.Failure, FileExportStatus.DownloadError))), Times.Once()); + _messageSubscriberService.Verify(p => p.Acknowledge(It.IsAny()), Times.Once()); + _messageSubscriberService.Verify(p => p.RequeueWithDelay(It.IsAny()), Times.Never()); + _messageSubscriberService.Verify(p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny()), Times.Once()); + + _logger.VerifyLogging("Error downloading payload.", LogLevel.Error, Times.AtLeastOnce()); + _logger.VerifyLoggingMessageBeginsWith("Error downloading payload. Waiting ", LogLevel.Error, Times.AtLeastOnce()); + } + + [RetryFact(2, 250, DisplayName = "success after Hl7 send")] + public async Task Success_After_Hl7Send() + { + _extAppScpLogger.Invocations.Clear(); + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var destination = new HL7DestinationEntity { HostIp = "192.168.0.0", Port = _port }; + var service = new Hl7ExportService(_logger.Object, _serviceScopeFactory.Object, _configuration, _dicomToolkit.Object); + + _messagePublisherService.Setup(p => p.Publish(It.IsAny(), It.IsAny())); + _messageSubscriberService.Setup(p => p.Acknowledge(It.IsAny())); + _messageSubscriberService.Setup(p => p.RequeueWithDelay(It.IsAny())); + _messageSubscriberService.Setup( + p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Callback, ushort>((topic, queue, messageReceivedCallback, prefetchCount) => + { + messageReceivedCallback(CreateMessageReceivedEventArgs("pacs")); + }); + + _storageService.Setup(p => p.GetObjectAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).ReturnsAsync(destination); + _dicomToolkit.Setup(p => p.Load(It.IsAny())).Returns(InstanceGenerator.GenerateDicomFile(sopInstanceUid: sopInstanceUid)); + + var dataflowCompleted = new ManualResetEvent(false); + service.ReportActionCompleted += (sender, args) => + { + dataflowCompleted.Set(); + }; + + DicomScpFixture.DicomStatus = DicomStatus.Success; + await service.StartAsync(_cancellationTokenSource.Token); + Assert.True(dataflowCompleted.WaitOne(5000)); + await StopAndVerify(service); + + _messagePublisherService.Verify( + p => p.Publish(It.IsAny(), + It.Is(match => CheckMessage(match, ExportStatus.Success, FileExportStatus.Success))), Times.Once()); + } + + + + + private bool CheckMessage(Message message, ExportStatus exportStatus, FileExportStatus fileExportStatus) + { + Guard.Against.Null(message, nameof(message)); + + var exportEvent = message.ConvertTo(); + return exportEvent.Status == exportStatus && + exportEvent.FileStatuses.First().Value == fileExportStatus; + } + + private static MessageReceivedEventArgs CreateMessageReceivedEventArgs(string destination) + { + var exportRequestEvent = new ExportRequestEvent + { + ExportTaskId = Guid.NewGuid().ToString(), + CorrelationId = Guid.NewGuid().ToString(), + Destinations = new string[] { destination }, + Files = new[] { "file1" }, + MessageId = Guid.NewGuid().ToString(), + WorkflowInstanceId = Guid.NewGuid().ToString(), + }; + var jsonMessage = new JsonMessage(exportRequestEvent, MessageBrokerConfiguration.InformaticsGatewayApplicationId, exportRequestEvent.CorrelationId, exportRequestEvent.DeliveryTag); + + return new MessageReceivedEventArgs(jsonMessage.ToMessage(), CancellationToken.None); + } + + private async Task StopAndVerify(Hl7ExportService service) + { + await service.StopAsync(_cancellationTokenSource.Token); + _logger.VerifyLogging($"{service.ServiceName} is stopping.", LogLevel.Information, Times.Once()); + await Task.Delay(500); + } + } +} diff --git a/src/InformaticsGateway/Test/Services/Export/ExportServiceBaseTest.cs b/src/InformaticsGateway/Test/Services/Export/ExportServiceBaseTest.cs index bc80f36c5..e076e06e8 100755 --- a/src/InformaticsGateway/Test/Services/Export/ExportServiceBaseTest.cs +++ b/src/InformaticsGateway/Test/Services/Export/ExportServiceBaseTest.cs @@ -20,11 +20,13 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Api.PlugIns; +using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Services.Export; using Monai.Deploy.InformaticsGateway.Services.Storage; @@ -54,8 +56,9 @@ public class TestExportService : ExportServiceBase public TestExportService( ILogger logger, IOptions InformaticsGatewayConfiguration, - IServiceScopeFactory serviceScopeFactory) - : base(logger, InformaticsGatewayConfiguration, serviceScopeFactory) + IServiceScopeFactory serviceScopeFactory, + IDicomToolkit _dicomToolkit) + : base(logger, InformaticsGatewayConfiguration, serviceScopeFactory, _dicomToolkit) { } @@ -70,6 +73,38 @@ protected override Task ExportDataBlockCallback(Export return Task.FromResult(exportRequestData); } + + protected override async Task ProcessMessage(MessageReceivedEventArgs eventArgs) + { + var (exportFlow, reportingActionBlock) = SetupActionBlocks(); + + lock (SyncRoot) + { + var exportRequest = eventArgs.Message.ConvertTo(); + if (ExportRequests.ContainsKey(exportRequest.ExportTaskId)) + { + return; + } + + exportRequest.MessageId = eventArgs.Message.MessageId; + exportRequest.DeliveryTag = eventArgs.Message.DeliveryTag; + + var exportRequestWithDetails = new ExportRequestEventDetails(exportRequest); + + ExportRequests.Add(exportRequest.ExportTaskId, exportRequestWithDetails); + if (!exportFlow.Post(exportRequestWithDetails)) + { + MessageSubscriber.Reject(eventArgs.Message); + } + else + { + } + } + + exportFlow.Complete(); + await reportingActionBlock.Completion.ConfigureAwait(false); + + } } public class ExportServiceBaseTest @@ -83,6 +118,7 @@ public class ExportServiceBaseTest private readonly IOptions _configuration; private readonly CancellationTokenSource _cancellationTokenSource; private readonly Mock _serviceScopeFactory; + private readonly Mock _dicomToolkit = new Mock(); public ExportServiceBaseTest() { @@ -121,7 +157,7 @@ public ExportServiceBaseTest() [RetryFact(5, 250, DisplayName = "Data flow test - can start/stop")] public async Task DataflowTest_StartStop() { - var service = new TestExportService(_logger.Object, _configuration, _serviceScopeFactory.Object); + var service = new TestExportService(_logger.Object, _configuration, _serviceScopeFactory.Object, _dicomToolkit.Object); await service.StartAsync(_cancellationTokenSource.Token); await StopAndVerify(service); @@ -145,7 +181,7 @@ public async Task DataflowTest_RejectOnInsufficientStorageSpace() messageReceivedCallback(CreateMessageReceivedEventArgs()); }); - var service = new TestExportService(_logger.Object, _configuration, _serviceScopeFactory.Object); + var service = new TestExportService(_logger.Object, _configuration, _serviceScopeFactory.Object, _dicomToolkit.Object); await service.StartAsync(_cancellationTokenSource.Token); await StopAndVerify(service); @@ -178,7 +214,7 @@ public async Task DataflowTest_PayloadDownlaodFailure() .ThrowsAsync(new Exception("storage error")); var countdownEvent = new CountdownEvent(1); - var service = new TestExportService(_logger.Object, _configuration, _serviceScopeFactory.Object); + var service = new TestExportService(_logger.Object, _configuration, _serviceScopeFactory.Object, _dicomToolkit.Object); service.ReportActionCompleted += (sender, e) => { countdownEvent.Signal(); @@ -224,7 +260,7 @@ public async Task DataflowTest_EndToEnd_WithPartialFailure() .ReturnsAsync(new MemoryStream(Encoding.UTF8.GetBytes(testData))); var countdownEvent = new CountdownEvent(5 * 3); - var service = new TestExportService(_logger.Object, _configuration, _serviceScopeFactory.Object); + var service = new TestExportService(_logger.Object, _configuration, _serviceScopeFactory.Object, _dicomToolkit.Object); service.ReportActionCompleted += (sender, e) => { countdownEvent.Signal(); @@ -238,7 +274,7 @@ public async Task DataflowTest_EndToEnd_WithPartialFailure() countdownEvent.Signal(); }; await service.StartAsync(_cancellationTokenSource.Token); - Assert.True(countdownEvent.Wait(1000000)); + Assert.True(countdownEvent.Wait(60000)); await StopAndVerify(service); _messagePublisherService.Verify( @@ -278,7 +314,7 @@ public async Task DataflowTest_EndToEnd() .ReturnsAsync(new MemoryStream(Encoding.UTF8.GetBytes(testData))); var countdownEvent = new CountdownEvent(5 * 3); - var service = new TestExportService(_logger.Object, _configuration, _serviceScopeFactory.Object); + var service = new TestExportService(_logger.Object, _configuration, _serviceScopeFactory.Object, _dicomToolkit.Object); service.ReportActionCompleted += (sender, e) => { countdownEvent.Signal(); @@ -290,7 +326,7 @@ public async Task DataflowTest_EndToEnd() countdownEvent.Signal(); }; await service.StartAsync(_cancellationTokenSource.Token); - Assert.True(countdownEvent.Wait(1000000)); + Assert.True(countdownEvent.Wait(60000)); await StopAndVerify(service); _messagePublisherService.Verify( diff --git a/src/InformaticsGateway/Test/Services/Export/ExtAppScuServiceTest.cs b/src/InformaticsGateway/Test/Services/Export/ExtAppScuServiceTest.cs new file mode 100755 index 000000000..689699ba6 --- /dev/null +++ b/src/InformaticsGateway/Test/Services/Export/ExtAppScuServiceTest.cs @@ -0,0 +1,595 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Ardalis.GuardClauses; +using FellowOakDicom; +using FellowOakDicom.Network; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api.Models; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; +using Monai.Deploy.InformaticsGateway.Common; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Monai.Deploy.InformaticsGateway.Services.Export; +using Monai.Deploy.InformaticsGateway.Services.Storage; +using Monai.Deploy.InformaticsGateway.SharedTest; +using Monai.Deploy.Messaging.API; +using Monai.Deploy.Messaging.Common; +using Monai.Deploy.Messaging.Events; +using Monai.Deploy.Messaging.Messages; +using Monai.Deploy.Storage.API; +using Moq; +using xRetry; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.Test.Services.Export +{ + [Collection("SCP Listener")] + public class ExtAppScuServiceTest + { + private readonly Mock _storageService; + private readonly Mock _messageSubscriberService; + private readonly Mock _messagePublisherService; + private readonly Mock _outputDataPlugInEngine; + private readonly Mock> _logger; + private readonly Mock _extAppScpLogger; + private readonly Mock _serviceScopeFactory; + private readonly IOptions _configuration; + private readonly Mock _dicomToolkit; + private readonly Mock _repository; + private readonly Mock _storageInfoProvider; + private readonly Mock _externalAppRepository = new Mock(); + + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly DicomScpFixture _dicomScp; + private readonly int _port = 1104; + + public ExtAppScuServiceTest(DicomScpFixture dicomScp) + { + _dicomScp = dicomScp ?? throw new ArgumentNullException(nameof(dicomScp)); + + _storageService = new Mock(); + _messageSubscriberService = new Mock(); + _messagePublisherService = new Mock(); + _outputDataPlugInEngine = new Mock(); + _logger = new Mock>(); + _extAppScpLogger = new Mock(); + _serviceScopeFactory = new Mock(); + _configuration = Options.Create(new InformaticsGatewayConfiguration()); + _dicomToolkit = new Mock(); + _cancellationTokenSource = new CancellationTokenSource(); + _repository = new Mock(); + _storageInfoProvider = new Mock(); + + var services = new ServiceCollection(); + services.AddScoped(p => _repository.Object); + services.AddScoped(p => _messagePublisherService.Object); + services.AddScoped(p => _messageSubscriberService.Object); + services.AddScoped(p => _outputDataPlugInEngine.Object); + services.AddScoped(p => _storageService.Object); + services.AddScoped(p => _storageInfoProvider.Object); + + var serviceProvider = services.BuildServiceProvider(); + + var scope = new Mock(); + scope.Setup(x => x.ServiceProvider).Returns(serviceProvider); + + _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(scope.Object); + DicomScpFixture.Logger = _extAppScpLogger.Object; + _dicomScp.Start(_port); + _configuration.Value.Export.Retries.DelaysMilliseconds = new[] { 1 }; + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + _storageInfoProvider.Setup(p => p.HasSpaceAvailableForExport).Returns(true); + + _outputDataPlugInEngine.Setup(p => p.Configure(It.IsAny>())); + _outputDataPlugInEngine.Setup(p => p.ExecutePlugInsAsync(It.IsAny())) + .Returns((ExportRequestDataMessage message) => Task.FromResult(message)); + + _externalAppRepository.Setup(r => r.GetAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult>(null)); + + var seriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var testDicom = InstanceGenerator.GenerateDicomFile(seriesInstanceUid: seriesInstanceUid); + _dicomToolkit.Setup(d => d.Load(It.IsAny())).Returns(testDicom); + } + + [RetryFact(5, 250, DisplayName = "Constructor - throws on null params")] + public void Constructor_ThrowsOnNullParams() + { + Assert.Throws(() => new ExtAppScuExportService(null, null, null, null, null)); + Assert.Throws(() => new ExtAppScuExportService(_logger.Object, null, null, null, null)); + Assert.Throws(() => new ExtAppScuExportService(_logger.Object, _serviceScopeFactory.Object, null, null, null)); + Assert.Throws(() => new ExtAppScuExportService(_logger.Object, _serviceScopeFactory.Object, _configuration, null, null)); + Assert.Throws(() => new ExtAppScuExportService(_logger.Object, _serviceScopeFactory.Object, _configuration, _dicomToolkit.Object, null)); + } + + [RetryFact(10, 250, DisplayName = "When no destination is defined")] + public async Task ShallFailWhenNoDestinationIsDefined() + { + _messagePublisherService.Setup(p => p.Publish(It.IsAny(), It.IsAny())); + _messageSubscriberService.Setup(p => p.Acknowledge(It.IsAny())); + _messageSubscriberService.Setup(p => p.RequeueWithDelay(It.IsAny())); + _messageSubscriberService.Setup( + p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Callback, ushort>(async (topic, queue, messageReceivedCallback, prefetchCount) => + { + await messageReceivedCallback(CreateMessageReceivedEventArgs(string.Empty)); + }); + + _storageService.Setup(p => p.GetObjectAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + + var service = new ExtAppScuExportService(_logger.Object, _serviceScopeFactory.Object, _configuration, _dicomToolkit.Object, _externalAppRepository.Object); + + var dataflowCompleted = new ManualResetEvent(false); + service.ReportActionCompleted += (sender, args) => + { + dataflowCompleted.Set(); + }; + + await service.StartAsync(_cancellationTokenSource.Token); + Assert.True(dataflowCompleted.WaitOne(3000)); + await StopAndVerify(service); + + _messagePublisherService.Verify( + p => p.Publish(It.IsAny(), + It.Is(match => CheckMessage(match, ExportStatus.Failure, FileExportStatus.ConfigurationError))), Times.Once()); + _messageSubscriberService.Verify(p => p.Acknowledge(It.IsAny()), Times.Once()); + _messageSubscriberService.Verify(p => p.RequeueWithDelay(It.IsAny()), Times.Never()); + _messageSubscriberService.Verify(p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny()), Times.Once()); + _logger.VerifyLogging("Export task does not have destination set.", LogLevel.Error, Times.Once()); + } + + [RetryFact(10, 250, DisplayName = "When destination is not configured")] + public async Task ShallFailWhenDestinationIsNotConfigured() + { + _messagePublisherService.Setup(p => p.Publish(It.IsAny(), It.IsAny())); + _messageSubscriberService.Setup(p => p.Acknowledge(It.IsAny())); + _messageSubscriberService.Setup(p => p.RequeueWithDelay(It.IsAny())); + _messageSubscriberService.Setup( + p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Callback, ushort>(async (topic, queue, messageReceivedCallback, prefetchCount) => + { + await messageReceivedCallback(CreateMessageReceivedEventArgs("pacs")); + }); + + _storageService.Setup(p => p.GetObjectAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).ReturnsAsync(default(DestinationApplicationEntity)); + + var service = new ExtAppScuExportService(_logger.Object, _serviceScopeFactory.Object, _configuration, _dicomToolkit.Object, _externalAppRepository.Object); + + var dataflowCompleted = new ManualResetEvent(false); + service.ReportActionCompleted += (sender, args) => + { + dataflowCompleted.Set(); + }; + + await service.StartAsync(_cancellationTokenSource.Token); + Assert.True(dataflowCompleted.WaitOne(3000)); + await StopAndVerify(service); + + _messagePublisherService.Verify( + p => p.Publish(It.IsAny(), + It.Is(match => CheckMessage(match, ExportStatus.Failure, FileExportStatus.ConfigurationError))), Times.Once()); + _messageSubscriberService.Verify(p => p.Acknowledge(It.IsAny()), Times.Once()); + _messageSubscriberService.Verify(p => p.RequeueWithDelay(It.IsAny()), Times.Never()); + _messageSubscriberService.Verify(p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny()), Times.Once()); + + _logger.VerifyLogging($"Specified destination 'pacs' does not exist.", LogLevel.Error, Times.Once()); + } + + [RetryFact(1, 250, DisplayName = "Association rejected")] + public async Task AssociationRejected() + { + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var destination = new DestinationApplicationEntity { AeTitle = "ABC", Name = DicomScpFixture.s_aETITLE, HostIp = "localhost", Port = _port }; + + _messagePublisherService.Setup(p => p.Publish(It.IsAny(), It.IsAny())); + _messageSubscriberService.Setup(p => p.Acknowledge(It.IsAny())); + _messageSubscriberService.Setup(p => p.RequeueWithDelay(It.IsAny())); + _messageSubscriberService.Setup( + p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Callback, ushort>(async (topic, queue, messageReceivedCallback, prefetchCount) => + { + await messageReceivedCallback(CreateMessageReceivedEventArgs("pacs")); + }); + + _storageService.Setup(p => p.GetObjectAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).ReturnsAsync(destination); + _dicomToolkit.Setup(p => p.Load(It.IsAny())).Returns(InstanceGenerator.GenerateDicomFile(sopInstanceUid: sopInstanceUid)); + + var service = new ExtAppScuExportService(_logger.Object, _serviceScopeFactory.Object, _configuration, _dicomToolkit.Object, _externalAppRepository.Object); + + var dataflowCompleted = new ManualResetEvent(false); + service.ReportActionCompleted += (sender, args) => + { + dataflowCompleted.Set(); + }; + + await service.StartAsync(_cancellationTokenSource.Token); + Assert.True(dataflowCompleted.WaitOne(5000)); + + await StopAndVerify(service); + _messagePublisherService.Verify( + p => p.Publish(It.IsAny(), + It.Is(match => CheckMessage(match, ExportStatus.Failure, FileExportStatus.ServiceError))), Times.Once()); + _messageSubscriberService.Verify(p => p.Acknowledge(It.IsAny()), Times.Once()); + _messageSubscriberService.Verify(p => p.RequeueWithDelay(It.IsAny()), Times.Never()); + _messageSubscriberService.Verify(p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny()), Times.Once()); + + _logger.VerifyLogging($"Association rejected.", LogLevel.Warning, Times.AtLeastOnce()); + _logger.VerifyLoggingMessageBeginsWith($"Association rejected with reason", LogLevel.Error, Times.Once()); + } + + [RetryFact(10, 250, DisplayName = "C-STORE simulate abort")] + public async Task SimulateAbort() + { + _extAppScpLogger.Invocations.Clear(); + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var destination = new DestinationApplicationEntity { AeTitle = "ABORT", Name = DicomScpFixture.s_aETITLE, HostIp = "localhost", Port = _port }; + var service = new ExtAppScuExportService(_logger.Object, _serviceScopeFactory.Object, _configuration, _dicomToolkit.Object, _externalAppRepository.Object); + + _messagePublisherService.Setup(p => p.Publish(It.IsAny(), It.IsAny())); + _messageSubscriberService.Setup(p => p.Acknowledge(It.IsAny())); + _messageSubscriberService.Setup(p => p.RequeueWithDelay(It.IsAny())); + _messageSubscriberService.Setup( + p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Callback, ushort>(async (topic, queue, messageReceivedCallback, prefetchCount) => + { + await messageReceivedCallback(CreateMessageReceivedEventArgs("pacs")); + }); + + _storageService.Setup(p => p.GetObjectAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).ReturnsAsync(destination); + _dicomToolkit.Setup(p => p.Load(It.IsAny())).Returns(InstanceGenerator.GenerateDicomFile(sopInstanceUid: sopInstanceUid)); + + var dataflowCompleted = new ManualResetEvent(false); + service.ReportActionCompleted += (sender, args) => + { + dataflowCompleted.Set(); + }; + + DicomScpFixture.DicomStatus = DicomStatus.ResourceLimitation; + await service.StartAsync(_cancellationTokenSource.Token); + Assert.True(dataflowCompleted.WaitOne(5000)); + + await StopAndVerify(service); + _messagePublisherService.Verify( + p => p.Publish(It.IsAny(), + It.Is(match => CheckMessage(match, ExportStatus.Failure, FileExportStatus.ServiceError))), Times.Once()); + _messageSubscriberService.Verify(p => p.Acknowledge(It.IsAny()), Times.Once()); + _messageSubscriberService.Verify(p => p.RequeueWithDelay(It.IsAny()), Times.Never()); + _messageSubscriberService.Verify(p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny()), Times.Once()); + + _logger.VerifyLoggingMessageBeginsWith($"Association aborted with reason", LogLevel.Error, Times.Once()); + } + + [RetryFact(10, 250, DisplayName = "C-STORE Failure")] + public async Task CStoreFailure() + { + _extAppScpLogger.Invocations.Clear(); + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var destination = new DestinationApplicationEntity { AeTitle = DicomScpFixture.s_aETITLE, Name = DicomScpFixture.s_aETITLE, HostIp = "localhost", Port = _port }; + var service = new ExtAppScuExportService(_logger.Object, _serviceScopeFactory.Object, _configuration, _dicomToolkit.Object, _externalAppRepository.Object); + + _messagePublisherService.Setup(p => p.Publish(It.IsAny(), It.IsAny())); + _messageSubscriberService.Setup(p => p.Acknowledge(It.IsAny())); + _messageSubscriberService.Setup(p => p.RequeueWithDelay(It.IsAny())); + _messageSubscriberService.Setup( + p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Callback, ushort>(async (topic, queue, messageReceivedCallback, prefetchCount) => + { + await messageReceivedCallback(CreateMessageReceivedEventArgs("pacs")); + }); + + _storageService.Setup(p => p.GetObjectAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).ReturnsAsync(destination); + _dicomToolkit.Setup(p => p.Load(It.IsAny())).Returns(InstanceGenerator.GenerateDicomFile(sopInstanceUid: sopInstanceUid)); + + var dataflowCompleted = new ManualResetEvent(false); + service.ReportActionCompleted += (sender, args) => + { + dataflowCompleted.Set(); + }; + + DicomScpFixture.DicomStatus = DicomStatus.ResourceLimitation; + await service.StartAsync(_cancellationTokenSource.Token); + Assert.True(dataflowCompleted.WaitOne(5000)); + await StopAndVerify(service); + + _messagePublisherService.Verify( + p => p.Publish(It.IsAny(), + It.Is(match => CheckMessage(match, ExportStatus.Failure, FileExportStatus.ServiceError))), Times.Once()); + _messageSubscriberService.Verify(p => p.Acknowledge(It.IsAny()), Times.Once()); + _messageSubscriberService.Verify(p => p.RequeueWithDelay(It.IsAny()), Times.Never()); + _messageSubscriberService.Verify(p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny()), Times.Once()); + _logger.VerifyLogging("Association accepted.", LogLevel.Information, Times.Once()); + _logger.VerifyLogging($"Failed to export with error {DicomStatus.ResourceLimitation}.", LogLevel.Error, Times.Once()); + } + + [RetryFact(10, 250, DisplayName = "Failed to load DICOM content")] + public async Task ErrorLoadingDicomContent() + { + + _extAppScpLogger.Invocations.Clear(); + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var destination = new DestinationApplicationEntity { AeTitle = DicomScpFixture.s_aETITLE, Name = DicomScpFixture.s_aETITLE, HostIp = "localhost", Port = _port }; + var service = new ExtAppScuExportService(_logger.Object, _serviceScopeFactory.Object, _configuration, _dicomToolkit.Object, _externalAppRepository.Object); + + _messagePublisherService.Setup(p => p.Publish(It.IsAny(), It.IsAny())); + _messageSubscriberService.Setup(p => p.Acknowledge(It.IsAny())); + _messageSubscriberService.Setup(p => p.RequeueWithDelay(It.IsAny())); + _messageSubscriberService.Setup( + p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Callback, ushort>(async (topic, queue, messageReceivedCallback, prefetchCount) => + { + await messageReceivedCallback(CreateMessageReceivedEventArgs("pacs")); + }); + + _storageService.Setup(p => p.GetObjectAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).ReturnsAsync(destination); + _dicomToolkit.Setup(p => p.Load(It.IsAny())).Throws(new Exception("error")); + + var dataflowCompleted = new ManualResetEvent(false); + service.ReportActionCompleted += (sender, args) => + { + dataflowCompleted.Set(); + }; + + DicomScpFixture.DicomStatus = DicomStatus.Success; + await service.StartAsync(_cancellationTokenSource.Token); + Assert.True(dataflowCompleted.WaitOne(5000)); + await StopAndVerify(service); + + _messagePublisherService.Verify( + p => p.Publish(It.IsAny(), + It.Is(match => CheckMessage(match, ExportStatus.Failure, FileExportStatus.UnsupportedDataType))), Times.Once()); + _messageSubscriberService.Verify(p => p.Acknowledge(It.IsAny()), Times.Once()); + _messageSubscriberService.Verify(p => p.RequeueWithDelay(It.IsAny()), Times.Never()); + _messageSubscriberService.Verify(p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny()), Times.Once()); + + _logger.VerifyLoggingMessageBeginsWith("Error reading DICOM file: error", LogLevel.Error, Times.Once()); + } + + [RetryFact(10, 250, DisplayName = "Unreachable Server")] + public async Task UnreachableServer() + { + _extAppScpLogger.Invocations.Clear(); + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var destination = new DestinationApplicationEntity { AeTitle = DicomScpFixture.s_aETITLE, Name = DicomScpFixture.s_aETITLE, HostIp = "UNKNOWNHOST123456789", Port = _port }; + var service = new ExtAppScuExportService(_logger.Object, _serviceScopeFactory.Object, _configuration, _dicomToolkit.Object, _externalAppRepository.Object); + + _messagePublisherService.Setup(p => p.Publish(It.IsAny(), It.IsAny())); + _messageSubscriberService.Setup(p => p.Acknowledge(It.IsAny())); + _messageSubscriberService.Setup(p => p.RequeueWithDelay(It.IsAny())); + _messageSubscriberService.Setup( + p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Callback, ushort>((topic, queue, messageReceivedCallback, prefetchCount) => + { + messageReceivedCallback(CreateMessageReceivedEventArgs("pacs")); + }); + + _storageService.Setup(p => p.GetObjectAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).ReturnsAsync(destination); + _dicomToolkit.Setup(p => p.Load(It.IsAny())).Returns(InstanceGenerator.GenerateDicomFile(sopInstanceUid: sopInstanceUid)); + + var dataflowCompleted = new ManualResetEvent(false); + service.ReportActionCompleted += (sender, args) => + { + dataflowCompleted.Set(); + }; + + DicomScpFixture.DicomStatus = DicomStatus.Success; + await service.StartAsync(_cancellationTokenSource.Token); + Assert.True(dataflowCompleted.WaitOne(8000)); + await StopAndVerify(service); + + _messagePublisherService.Verify( + p => p.Publish(It.IsAny(), + It.Is(match => CheckMessage(match, ExportStatus.Failure, FileExportStatus.ServiceError))), Times.Once()); + _messageSubscriberService.Verify(p => p.Acknowledge(It.IsAny()), Times.Once()); + _messageSubscriberService.Verify(p => p.RequeueWithDelay(It.IsAny()), Times.Never()); + _messageSubscriberService.Verify(p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny()), Times.Once()); + _logger.VerifyLoggingMessageBeginsWith("Association aborted with error", LogLevel.Error, Times.Once()); + } + + [RetryFact(10, 250, DisplayName = "C-STORE success")] + public async Task ExportCompletes() + { + _extAppScpLogger.Invocations.Clear(); + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var destination = new DestinationApplicationEntity { AeTitle = DicomScpFixture.s_aETITLE, Name = DicomScpFixture.s_aETITLE, HostIp = "localhost", Port = _port }; + var service = new ExtAppScuExportService(_logger.Object, _serviceScopeFactory.Object, _configuration, _dicomToolkit.Object, _externalAppRepository.Object); + + _messagePublisherService.Setup(p => p.Publish(It.IsAny(), It.IsAny())); + _messageSubscriberService.Setup(p => p.Acknowledge(It.IsAny())); + _messageSubscriberService.Setup(p => p.RequeueWithDelay(It.IsAny())); + _messageSubscriberService.Setup( + p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Callback, ushort>((topic, queue, messageReceivedCallback, prefetchCount) => + { + messageReceivedCallback(CreateMessageReceivedEventArgs("pacs")); + }); + + _storageService.Setup(p => p.GetObjectAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).ReturnsAsync(destination); + _dicomToolkit.Setup(p => p.Load(It.IsAny())).Returns(InstanceGenerator.GenerateDicomFile(sopInstanceUid: sopInstanceUid)); + + var dataflowCompleted = new ManualResetEvent(false); + service.ReportActionCompleted += (sender, args) => + { + dataflowCompleted.Set(); + }; + + DicomScpFixture.DicomStatus = DicomStatus.Success; + await service.StartAsync(_cancellationTokenSource.Token); + Assert.True(dataflowCompleted.WaitOne(5000)); + await StopAndVerify(service); + + _messagePublisherService.Verify( + p => p.Publish(It.IsAny(), + It.Is(match => CheckMessage(match, ExportStatus.Success, FileExportStatus.Success))), Times.Once()); + _messageSubscriberService.Verify(p => p.Acknowledge(It.IsAny()), Times.Once()); + _messageSubscriberService.Verify(p => p.RequeueWithDelay(It.IsAny()), Times.Never()); + _messageSubscriberService.Verify(p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny()), Times.Once()); + _logger.VerifyLogging("Association accepted.", LogLevel.Information, Times.Once()); + _logger.VerifyLogging($"Instance sent successfully.", LogLevel.Information, Times.Once()); + } + + [RetryFact(10, 250, DisplayName = "success save ExternalAppDetails")] + public async Task SaveExternalAppDetails() + { + _extAppScpLogger.Invocations.Clear(); + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var destination = new DestinationApplicationEntity { AeTitle = DicomScpFixture.s_aETITLE, Name = DicomScpFixture.s_aETITLE, HostIp = "localhost", Port = _port }; + var service = new ExtAppScuExportService(_logger.Object, _serviceScopeFactory.Object, _configuration, _dicomToolkit.Object, _externalAppRepository.Object); + + _messagePublisherService.Setup(p => p.Publish(It.IsAny(), It.IsAny())); + _messageSubscriberService.Setup(p => p.Acknowledge(It.IsAny())); + _messageSubscriberService.Setup(p => p.RequeueWithDelay(It.IsAny())); + _messageSubscriberService.Setup( + p => p.SubscribeAsync(It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Callback, ushort>((topic, queue, messageReceivedCallback, prefetchCount) => + { + messageReceivedCallback(CreateMessageReceivedEventArgs("pacs")); + }); + + _storageService.Setup(p => p.GetObjectAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).ReturnsAsync(destination); + _dicomToolkit.Setup(p => p.Load(It.IsAny())).Returns(InstanceGenerator.GenerateDicomFile(sopInstanceUid: sopInstanceUid)); + + var dataflowCompleted = new ManualResetEvent(false); + service.ReportActionCompleted += (sender, args) => + { + dataflowCompleted.Set(); + }; + + DicomScpFixture.DicomStatus = DicomStatus.Success; + await service.StartAsync(_cancellationTokenSource.Token); + Assert.True(dataflowCompleted.WaitOne(5000)); + await StopAndVerify(service); + + _externalAppRepository.Verify(r => r.AddAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + private bool CheckMessage(Message message, ExportStatus exportStatus, FileExportStatus fileExportStatus) + { + Guard.Against.Null(message, nameof(message)); + + var exportEvent = message.ConvertTo(); + return exportEvent.Status == exportStatus && + exportEvent.FileStatuses.First().Value == fileExportStatus; + } + + private static MessageReceivedEventArgs CreateMessageReceivedEventArgs(string destination) + { + var exportRequestEvent = new ExternalAppRequestEvent + { + ExportTaskId = Guid.NewGuid().ToString(), + CorrelationId = Guid.NewGuid().ToString(), + Targets = new List { new DataOrigin { Destination = destination } }, + Files = new[] { "file1" }, + MessageId = Guid.NewGuid().ToString(), + WorkflowInstanceId = Guid.NewGuid().ToString(), + }; + var jsonMessage = new JsonMessage(exportRequestEvent, MessageBrokerConfiguration.InformaticsGatewayApplicationId, exportRequestEvent.CorrelationId, exportRequestEvent.DeliveryTag); + + return new MessageReceivedEventArgs(jsonMessage.ToMessage(), CancellationToken.None); + } + + private async Task StopAndVerify(ExtAppScuExportService service) + { + await service.StopAsync(_cancellationTokenSource.Token); + _logger.VerifyLogging($"{service.ServiceName} is stopping.", LogLevel.Information, Times.Once()); + await Task.Delay(500); + } + } +} diff --git a/src/InformaticsGateway/Test/Services/Export/ScuExportServiceTest.cs b/src/InformaticsGateway/Test/Services/Export/ScuExportServiceTest.cs index ad538c544..a832401aa 100755 --- a/src/InformaticsGateway/Test/Services/Export/ScuExportServiceTest.cs +++ b/src/InformaticsGateway/Test/Services/Export/ScuExportServiceTest.cs @@ -27,7 +27,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; @@ -156,7 +156,7 @@ public async Task ShallFailWhenNoDestinationIsDefined() It.IsAny(), It.IsAny>(), It.IsAny()), Times.Once()); - _logger.VerifyLogging("SCU Export configuration error: Export task does not have destination set.", LogLevel.Error, Times.Once()); + _logger.VerifyLogging("Export task does not have destination set.", LogLevel.Error, Times.Once()); } [RetryFact(10, 250, DisplayName = "When destination is not configured")] @@ -202,7 +202,7 @@ public async Task ShallFailWhenDestinationIsNotConfigured() It.IsAny>(), It.IsAny()), Times.Once()); - _logger.VerifyLogging($"SCU Export configuration error: Specified destination 'pacs' does not exist.", LogLevel.Error, Times.Once()); + _logger.VerifyLogging($"Specified destination 'pacs' does not exist.", LogLevel.Error, Times.Once()); } [RetryFact(1, 250, DisplayName = "Association rejected")] diff --git a/src/InformaticsGateway/Test/Services/HealthLevel7/MllPExtractTests.cs b/src/InformaticsGateway/Test/Services/HealthLevel7/MllPExtractTests.cs new file mode 100755 index 000000000..0d9aa2541 --- /dev/null +++ b/src/InformaticsGateway/Test/Services/HealthLevel7/MllPExtractTests.cs @@ -0,0 +1,204 @@ + +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.Threading; +using Moq; +using Xunit; +using Microsoft.Extensions.Logging; +using System; +using Monai.Deploy.InformaticsGateway.Api; +using System.Collections.Generic; +using HL7.Dotnetcore; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Monai.Deploy.InformaticsGateway.Api.Mllp; +using Monai.Deploy.InformaticsGateway.Api.Storage; +using System.Threading.Tasks; +using Monai.Deploy.InformaticsGateway.Api.Models; +using FellowOakDicom; + +namespace Monai.Deploy.InformaticsGateway.Test.Services.HealthLevel7 +{ + public class MllPExtractTests + { + private const string SampleMessage = "MSH|^~\\&|MD|MD HOSPITAL|MD Test|MONAI Deploy|202207130000|SECURITY|MD^A01^ADT_A01|MSG00001|P|2.8||||\r\n"; + private const string ABCDEMessage = "MSH|^~\\&|Rayvolve|ABCDE|RIS|{InstitutionName}|{YYYYMMDDHHMMSS}||ORU^R01|{UniqueIdentifier}|P|2.5\r\nPID|{StudyInstanceUID}|{AccessionNumber}\r\nOBR|{StudyInstanceUID}||{AccessionNumber}|Rayvolve^{AlgorithmUsed}||||||||||||{AccessionNumber}|||||||F||{PriorityValues, ex: A^ASAP^HL70078}\r\nTQ1|||||||||{PriorityValues, ex: A^ASAP^HL70078}\r\nOBX|1|ST|113014^DICOM Study^DCM||{StudyInstanceUID}||||||O\r\nOBX|2|TX|59776-5^Procedure Findings^LN||{Textual findingsm, ex:\"Fracture detected\")}|||{Abnormal flag, ex : A^Abnormal^HL70078}|||F||||{ACR flag, ex : RID49482^Category 3 Non critical Actionable Finding^RadLex}\r\n"; + private const string VendorMessage = "MSH|^~\\&|Vendor INSIGHT CXR |Vendor Inc.|||20231130091315||ORU^R01|ORU20231130091315834|P|2.4||||||UNICODE UTF-8\r\nPID|1||2.25.82866891564990604580806081805518233357\r\nPV1|1|O\r\nORC|RE||||SC\r\nOBR|1|||CXR0001^Chest X-ray Report|||20230418142212.134||||||||||||||||||P|||||||Vendor\r\nNTE|1||Bilateral lungs are clear without remarkable opacity.\\X0A\\Cardiomediastinal contour appears normal.\\X0A\\Pneumothorax is not seen.\\X0A\\Pleural Effusion is present on the bilateral sides.\\X0A\\\\X0A\\Threshold value\\X0A\\Atelectasis: 15\\X0A\\Calcification: 15\\X0A\\Cardiomegaly: 15\\X0A\\Consolidation: 15\\X0A\\Fibrosis: 15\\X0A\\Mediastinal Widening: 15\\X0A\\Nodule: 15\\X0A\\Pleural Effusion: 15\\X0A\\Pneumoperitoneum: 15\\X0A\\Pneumothorax: 15\\X0A\\\r\nZDS|2.25.97606619386020057921123852316852071139||2.25.337759261491022538565548360794987622189|Vendor INSIGHT CXR|v3.1.5.3\r\nOBX|1|NM|RAB0001^Abnormality Score||50.82||||||P|||20230418142212.134||Vendor"; + + private readonly Mock> _logger; + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly MllpExtract _sut; + private readonly Mock _l7ApplicationConfigRepository = new Mock(); + private readonly Mock _externalAppDetailsRepository = new Mock(); + + public MllPExtractTests() + { + _logger = new Mock>(); + _cancellationTokenSource = new CancellationTokenSource(); + _sut = new MllpExtract(_l7ApplicationConfigRepository.Object, _externalAppDetailsRepository.Object, _logger.Object); + } + + [Fact(DisplayName = "Constructor Should Throw on missing arguments")] + public void Constructor_Should_Throw_on_missing_arguments() + { + Assert.Throws(() => new MllpExtract(null, null, null)); + Assert.Throws(() => new MllpExtract(_l7ApplicationConfigRepository.Object, null, null)); + Assert.Throws(() => new MllpExtract(_l7ApplicationConfigRepository.Object, _externalAppDetailsRepository.Object, null)); + + new MllpExtract(_l7ApplicationConfigRepository.Object, _externalAppDetailsRepository.Object, _logger.Object); + } + + [Fact(DisplayName = "ParseConfig Should Return Correct Item")] + public void ParseConfig_Should_Return_Correct_Item() + { + var correctid = new Guid("00000000-0000-0000-0000-000000000002"); + var azCorrectid = new Guid("00000000-0000-0000-0000-000000000001"); + var configs = new List { + new Hl7ApplicationConfigEntity{ Id= new Guid("00000000-0000-0000-0000-000000000001"), SendingId = new StringKeyValuePair{ Key = "MSH.4", Value = "ABCDE" } }, + new Hl7ApplicationConfigEntity{ Id= correctid, SendingId = new StringKeyValuePair{ Key = "MSH.4", Value = "MD HOSPITAL" } }, + }; + + var message = new Message(SampleMessage); + var isParsed = message.ParseMessage(); + + var config = MllpExtract.GetConfig(configs, message); + Assert.Equal(correctid, config?.Id); + + message = new Message(ABCDEMessage); + isParsed = message.ParseMessage(); + + config = MllpExtract.GetConfig(configs, message); + Assert.Equal(azCorrectid, config?.Id); + } + + [Fact(DisplayName = "Should Set MetaData On Hl7FileStorageMetadata Object")] + public async Task Should_Set_MetaData_On_Hl7FileStorageMetadata_Object() + { + var correctid = new Guid("00000000-0000-0000-0000-000000000002"); + var azCorrectid = new Guid("00000000-0000-0000-0000-000000000001"); + var configs = new List { + new Hl7ApplicationConfigEntity{ + Id= new Guid("00000000-0000-0000-0000-000000000001"), + SendingId = new StringKeyValuePair{ Key = "MSH.4", Value = "ABCDE" } + ,DataLink = new DataKeyValuePair{ Key = "PID.1", Value = DataLinkType.StudyInstanceUid } + }, + new Hl7ApplicationConfigEntity{ Id= correctid, SendingId = new StringKeyValuePair{ Key = "MSH.4", Value = "MD HOSPITAL" } }, + }; + + _l7ApplicationConfigRepository + .Setup(x => x.GetAllAsync(new CancellationToken())) + .ReturnsAsync(configs); + + _externalAppDetailsRepository.Setup(x => x.GetByStudyIdOutboundAsync("{StudyInstanceUID}", It.IsAny())) + .ReturnsAsync(new ExternalAppDetails + { + WorkflowInstanceId = "WorkflowInstanceId2", + ExportTaskID = "ExportTaskID2", + CorrelationId = "CorrelationId2", + DestinationFolder = "DestinationFolder2" + }); + + var message = new Message(ABCDEMessage); + var isParsed = message.ParseMessage(); + + var meatData = new Hl7FileStorageMetadata { Id = "metaId", File = new StorageObjectMetadata("txt") }; + + var configItem = await _sut.GetConfigItem(message); + await _sut.ExtractInfo(meatData, message, configItem); + + Assert.Equal("WorkflowInstanceId2", meatData.WorkflowInstanceId); + Assert.Equal("ExportTaskID2", meatData.TaskId); + Assert.Equal("CorrelationId2", meatData.CorrelationId); + Assert.StartsWith("DestinationFolder2", meatData.File.UploadPath); + } + + [Fact(DisplayName = "Should Set Original Patient And Study Uid")] + public async Task Should_Set_Original_Patient_And_Study_Uid() + { + var correctid = new Guid("00000000-0000-0000-0000-000000000002"); + var azCorrectid = new Guid("00000000-0000-0000-0000-000000000001"); + var configs = new List { + new Hl7ApplicationConfigEntity{ + Id= new Guid("00000000-0000-0000-0000-000000000001"), + SendingId = new StringKeyValuePair{ Key = "MSH.4", Value = "ABCDE" } + ,DataLink = new DataKeyValuePair{ Key = "PID.1", Value = DataLinkType.StudyInstanceUid }, + DataMapping = new List{ + new StringKeyValuePair { Key = "PID.1", Value = DicomTag.StudyInstanceUID.ToString() }, + new StringKeyValuePair { Key = "OBR.3", Value = DicomTag.PatientID.ToString() }, + } + + }, + new Hl7ApplicationConfigEntity{ Id= correctid, SendingId = new StringKeyValuePair{ Key = "MSH.4", Value = "MD HOSPITAL" } }, + }; + + _l7ApplicationConfigRepository + .Setup(x => x.GetAllAsync(new CancellationToken())) + .ReturnsAsync(configs); + + _externalAppDetailsRepository.Setup(x => x.GetByStudyIdOutboundAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new ExternalAppDetails + { + WorkflowInstanceId = "WorkflowInstanceId2", + ExportTaskID = "ExportTaskID2", + CorrelationId = "CorrelationId2", + DestinationFolder = "DestinationFolder2", + PatientId = "PatentID", + StudyInstanceUid = "StudyInstanceId" + }); + + var message = new Message(ABCDEMessage); + var isParsed = message.ParseMessage(); + + var te = message.GetValue("OBR.1"); + + var meatData = new Hl7FileStorageMetadata { Id = "metaId", File = new StorageObjectMetadata("txt") }; + + var configItem = await _sut.GetConfigItem(message); + message = await _sut.ExtractInfo(meatData, message, configItem); + + Assert.Equal("PatentID", message.GetValue("OBR.3")); + Assert.Equal("PatentID", message.GetValue("PID.2")); + Assert.Equal("StudyInstanceId", message.GetValue("PID.1")); + Assert.Equal("StudyInstanceId", message.GetValue("OBR.1")); + + } + + [Fact(DisplayName = "ParseConfig Should Return Correct Item for vendor")] + public void ParseConfig_Should_Return_Correct_Item_For_Vendor() + { + var correctid = new Guid("00000000-0000-0000-0000-000000000002"); + var azCorrectid = new Guid("00000000-0000-0000-0000-000000000001"); + + var configs = new List { + new Hl7ApplicationConfigEntity{ Id= new Guid("00000000-0000-0000-0000-000000000001"), SendingId = new StringKeyValuePair{ Key = "MSH.4", Value = "ABCDE" } }, + new Hl7ApplicationConfigEntity{ Id= correctid, SendingId = new StringKeyValuePair{ Key = "MSH.4", Value = "Vendor Inc." } }, + }; + + var message = new Message(VendorMessage); + var isParsed = message.ParseMessage(); + + var config = MllpExtract.GetConfig(configs, message); + Assert.Equal(correctid, config?.Id); + + message = new Message(ABCDEMessage); + isParsed = message.ParseMessage(); + + config = MllpExtract.GetConfig(configs, message); + Assert.Equal(azCorrectid, config?.Id); + } + } +} diff --git a/src/InformaticsGateway/Test/Services/HealthLevel7/MllpClientTest.cs b/src/InformaticsGateway/Test/Services/HealthLevel7/MllpClientTest.cs old mode 100644 new mode 100755 index f9900619c..de8b363cd --- a/src/InformaticsGateway/Test/Services/HealthLevel7/MllpClientTest.cs +++ b/src/InformaticsGateway/Test/Services/HealthLevel7/MllpClientTest.cs @@ -22,6 +22,7 @@ using System.Threading.Tasks; using HL7.Dotnetcore; using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api.Mllp; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Services.Common; using Monai.Deploy.InformaticsGateway.Services.HealthLevel7; @@ -38,6 +39,7 @@ public class MllpClientTest private readonly Hl7Configuration _config; private readonly Mock> _logger; private readonly CancellationTokenSource _cancellationTokenSource; + private readonly Mock _mIIpExtract = new Mock(); public MllpClientTest() { @@ -55,6 +57,7 @@ public void Constructor() Assert.Throws(() => new MllpClient(null, null, null)); Assert.Throws(() => new MllpClient(_tcpClient.Object, null, null)); Assert.Throws(() => new MllpClient(_tcpClient.Object, _config, null)); + Assert.Throws(() => new MllpClient(_tcpClient.Object, _config, null)); new MllpClient(_tcpClient.Object, _config, _logger.Object); } diff --git a/src/InformaticsGateway/Test/Services/HealthLevel7/MllpServiceTest.cs b/src/InformaticsGateway/Test/Services/HealthLevel7/MllpServiceTest.cs index 0c8abe39e..1f2119bc6 100755 --- a/src/InformaticsGateway/Test/Services/HealthLevel7/MllpServiceTest.cs +++ b/src/InformaticsGateway/Test/Services/HealthLevel7/MllpServiceTest.cs @@ -18,9 +18,9 @@ using System.Collections.Generic; using System.IO.Abstractions; using System.Net; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using HL7.Dotnetcore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -29,13 +29,16 @@ using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Services.Common; using Monai.Deploy.InformaticsGateway.Services.Connectors; -using Monai.Deploy.InformaticsGateway.Services.HealthLevel7; +using Monai.Deploy.InformaticsGateway.Api.Mllp; using Monai.Deploy.InformaticsGateway.Services.Storage; using Monai.Deploy.InformaticsGateway.SharedTest; using Monai.Deploy.Messaging.Events; using Moq; using xRetry; using Xunit; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; namespace Monai.Deploy.InformaticsGateway.Test.Services.HealthLevel7 { @@ -56,6 +59,9 @@ public class MllpServiceTest private readonly Mock> _logger; private readonly IServiceProvider _serviceProvider; private readonly Mock _storageInfoProvider; + private readonly Mock _mIIpExtract = new Mock(); + private readonly Mock _hl7DataPlugInEngine = new Mock(); + private readonly Mock _hl7ApplicationConfigRepository = new Mock(); public MllpServiceTest() { @@ -85,6 +91,9 @@ public MllpServiceTest() services.AddScoped(p => _payloadAssembler.Object); services.AddScoped(p => _fileSystem.Object); services.AddScoped(p => _storageInfoProvider.Object); + services.AddScoped(p => _mIIpExtract.Object); + services.AddScoped(p => _hl7DataPlugInEngine.Object); + services.AddScoped(p => _hl7ApplicationConfigRepository.Object); _serviceProvider = services.BuildServiceProvider(); _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); @@ -274,6 +283,9 @@ public async Task GivenATcpClientWithHl7Messages_WhenDisconnected_ExpectMessageT { var checkEvent = new ManualResetEventSlim(); var client = new Mock(); + _mIIpExtract.Setup(e => e.ExtractInfo(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((Hl7FileStorageMetadata meta, Message Msg, Hl7ApplicationConfigEntity configItem) => Msg); + _mllpClientFactory.Setup(p => p.CreateClient(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns(() => { @@ -308,5 +320,51 @@ public async Task GivenATcpClientWithHl7Messages_WhenDisconnected_ExpectMessageT _uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Exactly(3)); _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); } + + [RetryFact(10, 250)] + public async Task GivenATcpClientWithHl7Messages_WhenDisconnected_ExpectMessageToBeRePopulated() + { + var checkEvent = new ManualResetEventSlim(); + var client = new Mock(); + + _mIIpExtract.Setup(e => e.ExtractInfo(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((Hl7FileStorageMetadata meta, Message Msg, Hl7ApplicationConfigEntity configItem) => Msg); + + _mIIpExtract.Setup(e => e.GetConfigItem(It.IsAny())) + .ReturnsAsync((Message Msg) => new Hl7ApplicationConfigEntity()); + + _mllpClientFactory.Setup(p => p.CreateClient(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(() => + { + client.Setup(p => p.Start(It.IsAny>(), It.IsAny())) + .Callback, CancellationToken>((action, cancellationToken) => + { + var results = new MllpClientResult( + new List + { + new HL7.Dotnetcore.Message(""), + new HL7.Dotnetcore.Message(""), + new HL7.Dotnetcore.Message(""), + }, null); + action(client.Object, results); + checkEvent.Set(); + _cancellationTokenSource.Cancel(); + }); + client.Setup(p => p.Dispose()); + client.SetupGet(p => p.ClientId).Returns(Guid.NewGuid()); + return client.Object; + }); + + _tcpListener.Setup(p => p.AcceptTcpClientAsync(It.IsAny())) + .Returns(ValueTask.FromResult((new Mock()).Object)); + + var service = new MllpService(_serviceScopeFactory.Object, _options); + _ = service.StartAsync(_cancellationTokenSource.Token); + + Assert.True(checkEvent.Wait(3000)); + await Task.Delay(500).ConfigureAwait(false); + + _mIIpExtract.Verify(p => p.ExtractInfo(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + } } -} \ No newline at end of file +} diff --git a/src/InformaticsGateway/Test/Services/Http/DestinationAeTitleControllerTest.cs b/src/InformaticsGateway/Test/Services/Http/DestinationAeTitleControllerTest.cs old mode 100644 new mode 100755 index f57600a94..297c7362a --- a/src/InformaticsGateway/Test/Services/Http/DestinationAeTitleControllerTest.cs +++ b/src/InformaticsGateway/Test/Services/Http/DestinationAeTitleControllerTest.cs @@ -27,7 +27,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Logging; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; using Monai.Deploy.InformaticsGateway.Services.Common; diff --git a/src/InformaticsGateway/Test/Services/Http/DicomAssociationInfoControllerTest.cs b/src/InformaticsGateway/Test/Services/Http/DicomAssociationInfoControllerTest.cs old mode 100644 new mode 100755 index 1558ec691..89bfc3e7d --- a/src/InformaticsGateway/Test/Services/Http/DicomAssociationInfoControllerTest.cs +++ b/src/InformaticsGateway/Test/Services/Http/DicomAssociationInfoControllerTest.cs @@ -22,7 +22,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; using Monai.Deploy.InformaticsGateway.Services.Common.Pagination; diff --git a/src/InformaticsGateway/Test/Services/Http/Hl7ApplicationConfigControllerTest.cs b/src/InformaticsGateway/Test/Services/Http/Hl7ApplicationConfigControllerTest.cs new file mode 100755 index 000000000..34dc2a2a8 --- /dev/null +++ b/src/InformaticsGateway/Test/Services/Http/Hl7ApplicationConfigControllerTest.cs @@ -0,0 +1,322 @@ +/* + * Copyright 2021-2023 MONAI Consortium + * Copyright 2019-2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Database.Api; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Monai.Deploy.InformaticsGateway.Services.Http; +using Moq; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.Test.Services.Http +{ + public class Hl7ApplicationConfigControllerTest + { + private readonly Mock> _logger; + private readonly Mock _loggerFactory; + private readonly Hl7ApplicationConfigController _controller; + private readonly Mock _repo; + + public Hl7ApplicationConfigControllerTest() + { + _loggerFactory = new Mock(); + _logger = new Mock>(); + _repo = new Mock(); + _loggerFactory.Setup(p => p.CreateLogger(It.IsAny())).Returns(_logger.Object); + + _controller = new Hl7ApplicationConfigController( + _logger.Object, _repo.Object); + } + + private static Hl7ApplicationConfigEntity ValidApplicationEntity(string dicomStr) + { + var validApplicationEntity = new Hl7ApplicationConfigEntity() + { + Id = Guid.Empty, + DataLink = KeyValuePair.Create("testKey", DataLinkType.PatientId), + DataMapping = new() { KeyValuePair.Create("datamapkey", dicomStr) }, + SendingId = KeyValuePair.Create("sendingidkey", "sendingidvalue"), + DateTimeCreated = DateTime.UtcNow + }; + return validApplicationEntity; + } + + #region GET Tests + + [Fact] + public async Task Get_GiveExpectedInput_ReturnsOK() + { + _repo.Setup(r => r.GetAllAsync(It.IsAny())) + .ReturnsAsync(new List()); + var result = await _controller.Get(); + + var okResult = Assert.IsType(result); + var response = Assert.IsType>(okResult.Value); + Assert.Empty(response); + } + + [Fact] + public async Task Get_GiveExpectedInput_ReturnsNotFound() + { + _repo.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync((Hl7ApplicationConfigEntity)null); + var result = await _controller.Get("test"); + + Assert.IsType(result); + } + + [Fact] + public async Task Get_GiveExpectedInput_ReturnsOK2() + { + _repo.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync(new Hl7ApplicationConfigEntity()); + var result = await _controller.Get("test"); + + var okResult = Assert.IsType(result); + var response = Assert.IsType(okResult.Value); + Assert.NotNull(response); + } + #endregion + + #region DELETE Tests + + [Fact] + public async Task Delete_GiveExpectedInput_ReturnsNotFound() + { + _repo.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync((Hl7ApplicationConfigEntity)null); + var result = await _controller.Delete("test"); + + Assert.IsType(result); + } + + [Fact] + public async Task Delete_GiveExpectedInput_ReturnsOK() + { + _repo.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync(new Hl7ApplicationConfigEntity()); + _repo.Setup(r => r.DeleteAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new Hl7ApplicationConfigEntity()); + var result = await _controller.Delete("test"); + + var okResult = Assert.IsType(result); + var response = Assert.IsType(okResult.Value); + Assert.NotNull(response); + } + + [Fact] + public async Task Delete_GiveExpectedInput_ReturnsInternalServerError() + { + _repo.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync(new Hl7ApplicationConfigEntity()); + _repo.Setup(r => r.DeleteAsync(It.IsAny(), It.IsAny())) + .Throws(new DatabaseException()); + var result = await _controller.Delete("test"); + + var objectResult = Assert.IsType(result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + } + + [Fact] + public async Task Delete_GiveExpectedInput_ReturnsInternalServerError2() + { + _repo.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync(new Hl7ApplicationConfigEntity()); + _repo.Setup(r => r.DeleteAsync(It.IsAny(), It.IsAny())) + .Throws(new Exception()); + var result = await _controller.Delete("test"); + + var objectResult = Assert.IsType(result); + Assert.Equal(StatusCodes.Status500InternalServerError, objectResult.StatusCode); + } + + #endregion + + #region PUT Tests + + [Fact] + public async Task Put_GiveExpectedInput_ReturnsOK() + { + var validApplicationEntity = ValidApplicationEntity("0001,0001"); + _repo.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync(validApplicationEntity); + _repo.Setup(r => r.CreateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(validApplicationEntity); + var result = await _controller.Put(validApplicationEntity); + + var okResult = Assert.IsType(result); + var response = Assert.IsType(okResult.Value); + Assert.NotNull(response); + } + + [Fact] + public async Task Put_GiveExpectedInput_ReturnsBadRequest() + { + var result = await _controller.Put(null!); + + var objectResult = Assert.IsType(result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + } + + [Fact] + public async Task Put_GiveExpectedInput_ReturnsBadRequest2() + { + _repo.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync(new Hl7ApplicationConfigEntity()); + var result = await _controller.Put(new Hl7ApplicationConfigEntity()); + + var objectResult = Assert.IsType(result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + } + + [Fact] + public async Task Put_GiveExpectedInput_ReturnsInternalServerError() + { + var validApplicationEntity = ValidApplicationEntity("0001,0001"); + _repo.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync(validApplicationEntity); + _repo.Setup(r => r.UpdateAsync(It.IsAny(), It.IsAny())) + .Throws(new DatabaseException()); + var result = await _controller.Put(validApplicationEntity); + + var objectResult = Assert.IsType(result); + Assert.Equal(StatusCodes.Status500InternalServerError, objectResult.StatusCode); + } + + #endregion + + #region POST Tests + + [Theory] + [InlineData("(0001,0001)")] + [InlineData("0001,0001")] + [InlineData("(FFFE,E0DD)")] + [InlineData("FFFE,E0DD")] + public async Task Post_GiveExpectedInput_ReturnsOK(string dicomStr) + { + var validApplicationEntity = ValidApplicationEntity(dicomStr); + + _repo.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync(validApplicationEntity); + _repo.Setup(r => r.CreateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(validApplicationEntity); + var result = await _controller.Post(validApplicationEntity); + + var okResult = Assert.IsType(result); + var response = Assert.IsType(okResult.Value); + Assert.NotNull(response); + } + + [Theory] + [InlineData("(001,0001)")] + [InlineData("(0001,00x1")] + [InlineData("x001,0001)")] + [InlineData("00001,00001)")] + public async Task Post_GivenInvalidDicomValueInput_ReturnsBadRequest(string dicomStr) + { + //valid Hl7ApplicationEntity + var validApplicationEntity = new Hl7ApplicationConfigEntity() + { + Id = Guid.Empty, + DataLink = KeyValuePair.Create("testKey", DataLinkType.PatientId), + DataMapping = new() { KeyValuePair.Create("datamapkey", dicomStr) }, + SendingId = KeyValuePair.Create("sendingidkey", "sendingidvalue"), + DateTimeCreated = DateTime.UtcNow + }; + + _repo.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync(validApplicationEntity); + _repo.Setup(r => r.UpdateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(validApplicationEntity); + var result = await _controller.Post(validApplicationEntity); + + var objResult = Assert.IsType(result); + Assert.Equal(StatusCodes.Status400BadRequest, objResult.StatusCode); + } + + [Fact] + public async Task Post_GiveExpectedInput_ReturnsBadRequest() + { + var result = await _controller.Post(null!); + + var objectResult = Assert.IsType(result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + } + + [Fact] + public async Task Post_GiveExpectedInput_ReturnsBadRequest2() + { + _repo.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync((Hl7ApplicationConfigEntity)null); + var result = await _controller.Post(new Hl7ApplicationConfigEntity()); + + var objectResult = Assert.IsType(result); + + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + + } + + [Fact] + public async Task Post_GiveExpectedInput_ReturnsInternalServerError() + { + var validApplicationEntity = ValidApplicationEntity("0001,0001"); + _repo.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync(validApplicationEntity); + _repo.Setup(r => r.UpdateAsync(It.IsAny(), It.IsAny())) + .Throws(new DatabaseException()); + var result = await _controller.Post(validApplicationEntity); + + var objectResult = Assert.IsType(result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + } + [Fact] + public async Task Post_GiveExpectedInput_ReturnsInternalServerError3() + { + var validApplicationEntity = ValidApplicationEntity("0001,0001"); + _repo.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync(validApplicationEntity); + + var result = await _controller.Post(validApplicationEntity); + + var objectResult = Assert.IsType(result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + } + + [Fact] + public async Task Post_GiveExpectedInput_ReturnsInternalServerError2() + { + var validApplicationEntity = ValidApplicationEntity("0001,0001"); + _repo.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync(validApplicationEntity); + _repo.Setup(r => r.CreateAsync(It.IsAny(), It.IsAny())) + .Throws(new Exception()); + var result = await _controller.Post(validApplicationEntity); + + var objectResult = Assert.IsType(result); + Assert.Equal(StatusCodes.Status500InternalServerError, objectResult.StatusCode); + } + + #endregion + } +} diff --git a/src/InformaticsGateway/Test/Services/Http/MonaiAeTitleControllerTest.cs b/src/InformaticsGateway/Test/Services/Http/MonaiAeTitleControllerTest.cs index 24238c409..1f2058d57 100755 --- a/src/InformaticsGateway/Test/Services/Http/MonaiAeTitleControllerTest.cs +++ b/src/InformaticsGateway/Test/Services/Http/MonaiAeTitleControllerTest.cs @@ -27,7 +27,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Logging; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; using Monai.Deploy.InformaticsGateway.Services.Common; diff --git a/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityHandlerTest.cs b/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityHandlerTest.cs old mode 100644 new mode 100755 index a5f4dd9f6..0bf0cff4c --- a/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityHandlerTest.cs +++ b/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityHandlerTest.cs @@ -24,11 +24,12 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; using Monai.Deploy.InformaticsGateway.Services.Connectors; using Monai.Deploy.InformaticsGateway.Services.Scp; using Monai.Deploy.InformaticsGateway.Services.Storage; @@ -49,6 +50,7 @@ public class ApplicationEntityHandlerTest private readonly Mock _inputDataPlugInEngine; private readonly Mock _payloadAssembler; private readonly Mock _uploadQueue; + private readonly Mock _extAppDetailsRepo = new Mock(); private readonly IOptions _options; private readonly IFileSystem _fileSystem; private readonly IServiceProvider _serviceProvider; @@ -81,14 +83,15 @@ public ApplicationEntityHandlerTest() _options.Value.Storage.TemporaryDataStorage = TemporaryDataStorageLocation.Memory; } - [RetryFact(5, 250)] + [RetryFact(1, 250)] public void GivenAApplicationEntityHandler_WhenInitialized_ExpectParametersToBeValidated() { - Assert.Throws(() => new ApplicationEntityHandler(null, null, null)); - Assert.Throws(() => new ApplicationEntityHandler(_serviceScopeFactory.Object, null, null)); - Assert.Throws(() => new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, null)); + Assert.Throws(() => new ApplicationEntityHandler(null, null, null, null)); + Assert.Throws(() => new ApplicationEntityHandler(_serviceScopeFactory.Object, null, null, null)); + Assert.Throws(() => new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, null, null)); + Assert.Throws(() => new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options, null)); - _ = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options); + _ = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options, _extAppDetailsRepo.Object); } [RetryFact(5, 250)] @@ -102,13 +105,18 @@ public async Task GivenAApplicationEntityHandler_WhenHandleInstanceAsyncIsCalled IgnoredSopClasses = new List { DicomUID.SecondaryCaptureImageStorage.UID } }; - var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options); + var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options, _extAppDetailsRepo.Object); var request = GenerateRequest(); var dicomToolkit = new DicomToolkit(); var uids = dicomToolkit.GetStudySeriesSopInstanceUids(request.File); - await Assert.ThrowsAsync(async () => await handler.HandleInstanceAsync(request, aet.AeTitle, "CALLING", Guid.NewGuid(), uids)); + await Assert.ThrowsAsync(async + () => await handler.HandleInstanceAsync(request, + aet.AeTitle, + "CALLING", + Guid.NewGuid(), + uids, InformaticsGateway.Services.Common.ScpInputTypeEnum.WorkflowTrigger)); } [RetryFact(5, 250)] @@ -122,14 +130,14 @@ public async Task GivenACStoreRequest_WhenTheSopClassIsInTheIgnoreList_ExpectIns IgnoredSopClasses = new List { DicomUID.SecondaryCaptureImageStorage.UID } }; - var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options); + var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options, _extAppDetailsRepo.Object); handler.Configure(aet, Configuration.DicomJsonOptions.Complete, true); var request = GenerateRequest(); var dicomToolkit = new DicomToolkit(); var uids = dicomToolkit.GetStudySeriesSopInstanceUids(request.File); - await handler.HandleInstanceAsync(request, aet.AeTitle, "CALLING", Guid.NewGuid(), uids); + await handler.HandleInstanceAsync(request, aet.AeTitle, "CALLING", Guid.NewGuid(), uids, InformaticsGateway.Services.Common.ScpInputTypeEnum.WorkflowTrigger); _uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Never()); _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); @@ -146,14 +154,14 @@ public async Task GivenACStoreRequest_WhenTheSopClassIsNotInTheAllowedList_Expec AllowedSopClasses = new List { DicomUID.UltrasoundImageStorage.UID } }; - var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options); + var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options, _extAppDetailsRepo.Object); handler.Configure(aet, Configuration.DicomJsonOptions.Complete, true); var request = GenerateRequest(); var dicomToolkit = new DicomToolkit(); var uids = dicomToolkit.GetStudySeriesSopInstanceUids(request.File); - await handler.HandleInstanceAsync(request, aet.AeTitle, "CALLING", Guid.NewGuid(), uids); + await handler.HandleInstanceAsync(request, aet.AeTitle, "CALLING", Guid.NewGuid(), uids, InformaticsGateway.Services.Common.ScpInputTypeEnum.WorkflowTrigger); _uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Never()); _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); @@ -170,7 +178,7 @@ public async Task GivenACStoreRequest_WhenHandleInstanceAsyncIsCalled_ExpectADic PlugInAssemblies = new List() { typeof(TestInputDataPlugInAddWorkflow).AssemblyQualifiedName } }; - var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options); + var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options, _extAppDetailsRepo.Object); handler.Configure(aet, Configuration.DicomJsonOptions.Complete, true); var request = GenerateRequest(); @@ -179,7 +187,7 @@ public async Task GivenACStoreRequest_WhenHandleInstanceAsyncIsCalled_ExpectADic _inputDataPlugInEngine.Setup(p => p.ExecutePlugInsAsync(It.IsAny(), It.IsAny())) .Returns((DicomFile dicomFile, FileStorageMetadata fileMetadata) => Task.FromResult(new Tuple(dicomFile, fileMetadata))); - await handler.HandleInstanceAsync(request, aet.AeTitle, "CALLING", Guid.NewGuid(), uids); + await handler.HandleInstanceAsync(request, aet.AeTitle, "CALLING", Guid.NewGuid(), uids, InformaticsGateway.Services.Common.ScpInputTypeEnum.WorkflowTrigger); _uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Once()); _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); @@ -203,7 +211,7 @@ public void GivenAConfiguredAETitle_WhenConfiguringAgainWithDifferentAETitle_Exp Name = "TESTAET", Workflows = new List() { "AppA", "AppB", Guid.NewGuid().ToString() } }; - var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options); + var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options, _extAppDetailsRepo.Object); handler.Configure(aet, Configuration.DicomJsonOptions.Complete, true); newAet.AeTitle = "NewAETitle"; @@ -228,4 +236,4 @@ private static DicomCStoreRequest GenerateRequest() return new DicomCStoreRequest(file); } } -} \ No newline at end of file +} diff --git a/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityManagerTest.cs b/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityManagerTest.cs old mode 100644 new mode 100755 index 8f90693bb..df9934201 --- a/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityManagerTest.cs +++ b/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityManagerTest.cs @@ -26,9 +26,11 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Monai.Deploy.InformaticsGateway.Services.Common; using Monai.Deploy.InformaticsGateway.Services.Scp; using Monai.Deploy.InformaticsGateway.Services.Storage; using Monai.Deploy.InformaticsGateway.SharedTest; @@ -108,7 +110,7 @@ public async Task HandleCStoreRequest_ShallThrowIfAENotConfigured() var request = GenerateRequest(); var exception = await Assert.ThrowsAsync(async () => { - await manager.HandleCStoreRequest(request, "BADAET", "CallingAET", Guid.NewGuid()); + await manager.HandleCStoreRequest(request, "BADAET", "CallingAET", Guid.NewGuid(), InformaticsGateway.Services.Common.ScpInputTypeEnum.WorkflowTrigger); }); Assert.Equal("Called AE Title 'BADAET' is not configured", exception.Message); @@ -138,7 +140,7 @@ public async Task HandleCStoreRequest_ThrowWhenOnLowStorageSpace() var request = GenerateRequest(); await Assert.ThrowsAsync(async () => { - await manager.HandleCStoreRequest(request, aet, "CallingAET", Guid.NewGuid()); + await manager.HandleCStoreRequest(request, aet, "CallingAET", Guid.NewGuid(), InformaticsGateway.Services.Common.ScpInputTypeEnum.WorkflowTrigger); }); _logger.VerifyLogging($"{aet} added to AE Title Manager.", LogLevel.Information, Times.Once()); @@ -297,7 +299,7 @@ public async Task ShallHandleCStoreRequest() Assert.True(await manager.IsAeTitleConfiguredAsync("AE1").ConfigureAwait(false)); var request = GenerateRequest(); - await manager.HandleCStoreRequest(request, "AE1", "AE", associationId); + await manager.HandleCStoreRequest(request, "AE1", "AE", associationId, InformaticsGateway.Services.Common.ScpInputTypeEnum.WorkflowTrigger); _applicationEntityHandler.Verify(p => p.HandleInstanceAsync( @@ -309,7 +311,8 @@ public async Task ShallHandleCStoreRequest() p.SopClassUid.Equals(request.Dataset.GetSingleValue(DicomTag.SOPClassUID)) && p.StudyInstanceUid.Equals(request.Dataset.GetSingleValue(DicomTag.StudyInstanceUID)) && p.SeriesInstanceUid.Equals(request.Dataset.GetSingleValue(DicomTag.SeriesInstanceUID)) && - p.SopInstanceUid.Equals(request.Dataset.GetSingleValue(DicomTag.SOPInstanceUID)))) + p.SopInstanceUid.Equals(request.Dataset.GetSingleValue(DicomTag.SOPInstanceUID))), + ScpInputTypeEnum.WorkflowTrigger) , Times.Once()); } diff --git a/src/InformaticsGateway/Test/Services/Scp/MonaiAeChangedNotificationServiceTest.cs b/src/InformaticsGateway/Test/Services/Scp/MonaiAeChangedNotificationServiceTest.cs old mode 100644 new mode 100755 index 299a85afe..9dca2a3ca --- a/src/InformaticsGateway/Test/Services/Scp/MonaiAeChangedNotificationServiceTest.cs +++ b/src/InformaticsGateway/Test/Services/Scp/MonaiAeChangedNotificationServiceTest.cs @@ -16,7 +16,7 @@ using System; using Microsoft.Extensions.Logging; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Services.Scp; using Moq; using xRetry; diff --git a/src/InformaticsGateway/Test/Services/Scp/ScpServiceTest.cs b/src/InformaticsGateway/Test/Services/Scp/ScpServiceTest.cs index c8209a0f3..42e4043f5 100755 --- a/src/InformaticsGateway/Test/Services/Scp/ScpServiceTest.cs +++ b/src/InformaticsGateway/Test/Services/Scp/ScpServiceTest.cs @@ -28,6 +28,7 @@ using Monai.Deploy.InformaticsGateway.Api.Rest; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Services.Common; using Monai.Deploy.InformaticsGateway.Services.Scp; using Monai.Deploy.InformaticsGateway.SharedTest; using Moq; @@ -234,7 +235,7 @@ public async Task CStore_OnCStoreRequest_InsufficientStorageAvailableException() _associationDataProvider.Setup(p => p.IsValidSourceAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); _associationDataProvider.Setup(p => p.IsAeTitleConfiguredAsync(It.IsAny())).ReturnsAsync(true); _associationDataProvider.Setup(p => p.CanStore).Returns(true); - _associationDataProvider.Setup(p => p.HandleCStoreRequest(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Throws(new InsufficientStorageAvailableException()); + _associationDataProvider.Setup(p => p.HandleCStoreRequest(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Throws(new InsufficientStorageAvailableException()); var countdownEvent = new CountdownEvent(3); var service = await CreateService(); @@ -266,7 +267,7 @@ public async Task CStore_OnCStoreRequest_IoException() _associationDataProvider.Setup(p => p.IsValidSourceAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); _associationDataProvider.Setup(p => p.IsAeTitleConfiguredAsync(It.IsAny())).ReturnsAsync(true); _associationDataProvider.Setup(p => p.CanStore).Returns(true); - _associationDataProvider.Setup(p => p.HandleCStoreRequest(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Throws(new IOException { HResult = Constants.ERROR_HANDLE_DISK_FULL }); + _associationDataProvider.Setup(p => p.HandleCStoreRequest(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Throws(new IOException { HResult = Constants.ERROR_HANDLE_DISK_FULL }); var countdownEvent = new CountdownEvent(3); var service = await CreateService(); @@ -297,7 +298,7 @@ public async Task CStore_OnCStoreRequest_Exception() _associationDataProvider.Setup(p => p.IsValidSourceAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); _associationDataProvider.Setup(p => p.IsAeTitleConfiguredAsync(It.IsAny())).ReturnsAsync(true); _associationDataProvider.Setup(p => p.CanStore).Returns(true); - _associationDataProvider.Setup(p => p.HandleCStoreRequest(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Throws(new Exception()); + _associationDataProvider.Setup(p => p.HandleCStoreRequest(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Throws(new Exception()); var countdownEvent = new CountdownEvent(3); var service = await CreateService(); @@ -329,7 +330,7 @@ public async Task CStore_OnCStoreRequest_Success() _associationDataProvider.Setup(p => p.IsValidSourceAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); _associationDataProvider.Setup(p => p.IsAeTitleConfiguredAsync(It.IsAny())).ReturnsAsync(true); _associationDataProvider.Setup(p => p.CanStore).Returns(true); - _associationDataProvider.Setup(p => p.HandleCStoreRequest(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + _associationDataProvider.Setup(p => p.HandleCStoreRequest(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); var countdownEvent = new CountdownEvent(3); var service = await CreateService(); @@ -361,7 +362,7 @@ public async Task CStore_OnClientAbort() _associationDataProvider.Setup(p => p.IsValidSourceAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); _associationDataProvider.Setup(p => p.IsAeTitleConfiguredAsync(It.IsAny())).ReturnsAsync(true); _associationDataProvider.Setup(p => p.CanStore).Returns(true); - _associationDataProvider.Setup(p => p.HandleCStoreRequest(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + _associationDataProvider.Setup(p => p.HandleCStoreRequest(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); var countdownEvent = new CountdownEvent(1); var service = await CreateService(); @@ -380,6 +381,41 @@ public async Task CStore_OnClientAbort() _logger.VerifyLogging($"Aborted {DicomAbortSource.ServiceUser} with reason {DicomAbortReason.NotSpecified}.", LogLevel.Warning, Times.Once()); } + [RetryFact(5, 250, DisplayName = "C-STORE - ExternalApp OnCStoreRequest - SendType")] + public async Task CStore_OnCStoreRequest_SendsType() + { + ScpInputTypeEnum savedType = ScpInputTypeEnum.WorkflowTrigger; + _associationDataProvider.Setup(p => p.IsValidSourceAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); + _associationDataProvider.Setup(p => p.IsAeTitleConfiguredAsync(It.IsAny())).ReturnsAsync(true); + _associationDataProvider.Setup(p => p.CanStore).Returns(true); + _associationDataProvider.Setup(p => p.HandleCStoreRequest(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((DicomCStoreRequest request, string b, string c, Guid d, ScpInputTypeEnum type) => savedType = type); + + var countdownEvent = new CountdownEvent(3); + var service = await CreateExternalAppService(); + + var client = DicomClientFactory.Create("localhost", _configuration.Value.Dicom.Scp.ExternalAppPort, false, "STORESCU", "STORESCP"); + var request = new DicomCStoreRequest(InstanceGenerator.GenerateDicomFile()); + await client.AddRequestAsync(request); + client.AssociationAccepted += (sender, e) => + { + countdownEvent.Signal(); + }; + client.AssociationReleased += (sender, e) => + { + countdownEvent.Signal(); + }; + request.OnResponseReceived += (DicomCStoreRequest request, DicomCStoreResponse response) => + { + Assert.Equal(DicomStatus.Success, response.Status); + countdownEvent.Signal(); + }; + + await client.SendAsync(); + Assert.True(countdownEvent.Wait(2000)); + Assert.Equal(ScpInputTypeEnum.ExternalAppReturn, savedType); + } + private async Task CreateService() { var tryCount = 0; @@ -400,5 +436,26 @@ private async Task CreateService() Assert.Equal(ServiceStatus.Running, service.Status); return service; } + + private async Task CreateExternalAppService() + { + var tryCount = 0; + ExternalAppScpService service = null; + + do + { + _configuration.Value.Dicom.Scp.ExternalAppPort = Interlocked.Increment(ref s_nextPort); + if (service != null) + { + service.Dispose(); + await Task.Delay(100); + } + service = new ExternalAppScpService(_serviceScopeFactory.Object, _associationDataProvider.Object, _appLifetime.Object, _configuration); + _ = service.StartAsync(_cancellationTokenSource.Token); + } while (service.Status != ServiceStatus.Running && tryCount++ < 5); + + Assert.Equal(ServiceStatus.Running, service.Status); + return service; + } } } diff --git a/src/InformaticsGateway/Test/appsettings.json b/src/InformaticsGateway/Test/appsettings.json old mode 100644 new mode 100755 index f3e7bf5c3..c600ee131 --- a/src/InformaticsGateway/Test/appsettings.json +++ b/src/InformaticsGateway/Test/appsettings.json @@ -10,6 +10,7 @@ "dicom": { "scp": { "port": 1104, + "externalAppPort": 1106, "logDimseDatasets": false, "rejectUnknownSources": true }, @@ -35,7 +36,6 @@ "password": "password", "virtualHost": "monaideploy", "exchange": "monaideploy", - "exportRequestQueue": "export_tasks" } }, "storage": { diff --git a/src/InformaticsGateway/Test/packages.lock.json b/src/InformaticsGateway/Test/packages.lock.json index d9bcbd3d0..0cbb69366 100755 --- a/src/InformaticsGateway/Test/packages.lock.json +++ b/src/InformaticsGateway/Test/packages.lock.json @@ -8,6 +8,15 @@ "resolved": "6.0.0", "contentHash": "tW3lsNS+dAEII6YGUX/VMoJjBS1QvsxqJeqLaJXub08y1FSjasFPtQ4UBUsudE9PNrzLjooClMsPtY2cZLdXpQ==" }, + "FluentAssertions": { + "type": "Direct", + "requested": "[6.11.0, )", + "resolved": "6.11.0", + "contentHash": "aBaagwdNtVKkug1F3imGXUlmoBd8ZUZX8oQ5niThaJhF79SpESe1Gzq7OFuZkQdKD5Pa4Mone+jrbas873AT4g==", + "dependencies": { + "System.Configuration.ConfigurationManager": "4.4.0" + } + }, "Microsoft.AspNetCore.Mvc.WebApiCompatShim": { "type": "Direct", "requested": "[2.2.0, )", @@ -22,11 +31,11 @@ }, "Microsoft.EntityFrameworkCore.InMemory": { "type": "Direct", - "requested": "[6.0.22, )", - "resolved": "6.0.22", - "contentHash": "CcL5ajX+/OkafcP5OMplCBnIgSfaQy5BUjEZQKZ9BlspnwFFucy+wcE0LL1ycOlWcDYGI42FnQ45dD1Kcz+ZKA==", + "requested": "[6.0.25, )", + "resolved": "6.0.25", + "contentHash": "T1wFaHL0WS51PlrSzWfBX2qppMbuIserPUaSwrw6Uhvg4WllsQPKYqFGAZC9bbUAihjgY5es7MIgSEtXYNdLiw==", "dependencies": { - "Microsoft.EntityFrameworkCore": "6.0.22" + "Microsoft.EntityFrameworkCore": "6.0.25" } }, "Microsoft.NET.Test.Sdk": { @@ -410,19 +419,19 @@ }, "Microsoft.Data.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "gtIGHbGnRq/h4mFSJYr9BdMObvJV/a67nBubs50VjPDusQARtWJzeVTirDWsbL1qTvGzbbZCD7VE7+s2ixZfow==", + "resolved": "6.0.25", + "contentHash": "rbXNoMg/ylGyJxLcyetojuXFzvDG85M31DfFbqL8veN4P8oG6wmnPwWNn3/bDIEDVvdw15R092dxpobQeQcjGg==", "dependencies": { "SQLitePCLRaw.core": "2.1.2" } }, "Microsoft.EntityFrameworkCore": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "vNe+y8ZsEf1CsfmfYttfKAz/IgCCtphgguvao0HWNJNdjZf9cabD288nZJ17b/WaQMWXhLwYAsofk8vNVkfTOA==", + "resolved": "6.0.25", + "contentHash": "txcqw2xrmvMoTIgzAdUk8JHLELofGgTK3i6glswVZs4SC8BOU1M/iSAtwMIVtAtfzxuBIUAbHPx+Ly6lfkYe7g==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "6.0.22", - "Microsoft.EntityFrameworkCore.Analyzers": "6.0.22", + "Microsoft.EntityFrameworkCore.Abstractions": "6.0.25", + "Microsoft.EntityFrameworkCore.Analyzers": "6.0.25", "Microsoft.Extensions.Caching.Memory": "6.0.1", "Microsoft.Extensions.DependencyInjection": "6.0.1", "Microsoft.Extensions.Logging": "6.0.0", @@ -432,39 +441,39 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "82SZfdrLe7bdDB8/3INV0UULvlUzsdHkrEYylDCrzFXRWHXG9eO5jJQjRHU8j9XkGIN+MSPgIlczBnqeDvB36A==" + "resolved": "6.0.25", + "contentHash": "i6UpdWqWxSBbIFOkaMoubM40yIjTZO+0rIUkY5JRltSeFI4PzncBBQcNVNXXjAmiLXF/xY0xTS+ykClbkV46Yg==" }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "W7yfdEbEuS1OPPxU0EOA6haqI4uvzs7OwHKh81DiJFn3NFNP2ztSovkOzBDhTwHX0j+OySsAj3BEJhuzTVYIVw==", + "resolved": "6.0.25", + "contentHash": "ci2lR++x7R7LR71+HoeRnB9Z5VeOQ1ILLbFRhsjjWZyLrAMkdq7TK9Ll47jo1TXDWF8Ddeap1JgcptgPKkWSRA==", "dependencies": { - "Microsoft.EntityFrameworkCore": "6.0.22", + "Microsoft.EntityFrameworkCore": "6.0.25", "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" } }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "EDKnYZtxq7P131xxLsEokda86WnFRiVAveLVAYR8kzyWl/UwTpf/RS2m2FrbH/U8vX3A+IQNpabtxcjtCUrY0g==", + "resolved": "6.0.25", + "contentHash": "vaQNuXgUN0nIzFXQiPSb9iAaJqLvZA164Sx9mjF5rFQS5cwQ/AiymF0e4J0QH3P07Mf3zEVZE5u2fTO0NacuMQ==", "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.22", + "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.25", "SQLitePCLRaw.bundle_e_sqlite3": "2.1.2" } }, "Microsoft.EntityFrameworkCore.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "xSU77ORQgwlD+s5Cmlk9DzoSCu5oxlHLuQl+v5zAZ0Uv5yH17hp02TBfz3x9nBA+CrIsqaLjGEuyZmLDf/5ATw==", + "resolved": "6.0.25", + "contentHash": "IU4E8I9FS2sUVxJJ0w/4jogLQ8C0zvu/SO6b1tRmiiCtTrHhjUB0tqhxjrFnDXZ/mpCJOElw50+qhbcElm0CYw==", "dependencies": { - "Microsoft.Data.Sqlite.Core": "6.0.22", - "Microsoft.EntityFrameworkCore.Relational": "6.0.22", + "Microsoft.Data.Sqlite.Core": "6.0.25", + "Microsoft.EntityFrameworkCore.Relational": "6.0.25", "Microsoft.Extensions.DependencyModel": "6.0.0" } }, @@ -570,10 +579,10 @@ }, "Microsoft.Extensions.Diagnostics.HealthChecks": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "HB1Zp1NY9m+HwYKLZBgUfNIt0xXzm4APARDuAIPODl8pT4g10oOiEDN8asOzx/sfL9xM+Sse5Zne9L+6qYi/iA==", + "resolved": "6.0.25", + "contentHash": "9vz47iGkzqhh0bGqomOTxaJNEEajeNcbSTSWwhh9Soo9lWm0UdPbw04CxXCQJPhc0aw9OaMnOxx7sCcde8/adA==", "dependencies": { - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.22", + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.25", "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", "Microsoft.Extensions.Logging.Abstractions": "6.0.4", "Microsoft.Extensions.Options": "6.0.0" @@ -581,17 +590,17 @@ }, "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "yvz+0r3qAt6gNEKlGSBO1BXMhtD3Tt8yzU59dHASolpwlSHvgqy0tEP6KXn3MPoKlPr0CiAHUdzOwYSoljzRSg==" + "resolved": "6.0.25", + "contentHash": "9sd1K/rp/vlxrBWNa0i8fgHCBPg94cocGMsJr7z9e2zQGQxMHNGpspdcy/FRGPAh2CINQet/RrM6Ef196xI20w==" }, "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "PNj+/e/GCJh3ZNzxEGhkMpKJgmmbuGar6Uk/R3mPFZacTx6lBdLs4Ev7uf4XQWqTdJe56rK+2P3oF/9jIGbxgw==", + "resolved": "6.0.25", + "contentHash": "Cmhq0sgb53+dh9xHOlBEQUhi13vsZeQ4fcYC9JYO4med7pabj9x3100opCdUv+7UX+tUC1GPm/nco+1skJdLFA==", "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "6.0.22", - "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.22", - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.22" + "Microsoft.EntityFrameworkCore.Relational": "6.0.25", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.25", + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.25" } }, "Microsoft.Extensions.FileProviders.Abstractions": { @@ -807,8 +816,8 @@ }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -818,10 +827,10 @@ }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } @@ -1234,6 +1243,14 @@ "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, + "System.Configuration.ConfigurationManager": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "gWwQv/Ug1qWJmHCmN17nAbxJYmQBM/E94QxKLksvUiiKB1Ld3Sc/eK1lgmbSjDFxkQhVuayI/cGFZhpBSodLrg==", + "dependencies": { + "System.Security.Cryptography.ProtectedData": "4.4.0" + } + }, "System.Console": { "type": "Transitive", "resolved": "4.3.0", @@ -1819,6 +1836,11 @@ "System.Threading.Tasks": "4.3.0" } }, + "System.Security.Cryptography.ProtectedData": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "cJV7ScGW7EhatRsjehfvvYVBvtiSMKgN8bOVI0bQhnF5bU7vnHVIsH49Kva7i7GWaWYvmEzkYVk1TC+gZYBEog==" + }, "System.Security.Cryptography.X509Certificates": { "type": "Transitive", "resolved": "4.3.0", @@ -2053,7 +2075,7 @@ "Monai.Deploy.InformaticsGateway.Database.EntityFramework": "[1.0.0, )", "Monai.Deploy.InformaticsGateway.DicomWeb.Client": "[1.0.0, )", "Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution": "[1.0.0, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Security": "[0.1.3, )", "Monai.Deploy.Storage.MinIO": "[0.2.18, )", "NLog.Web.AspNetCore": "[5.3.4, )", @@ -2063,11 +2085,12 @@ "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { + "HL7-dotnetcore": "[2.36.0, )", "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.22, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.25, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[1.0.4, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging": "[1.0.5, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Storage": "[0.2.18, )", "fo-dicom": "[5.1.1, )" } @@ -2096,7 +2119,7 @@ "type": "Project", "dependencies": { "AspNetCore.HealthChecks.MongoDb": "[6.0.2, )", - "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "[6.0.22, )", + "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "[6.0.25, )", "Microsoft.Extensions.Options.ConfigurationExtensions": "[6.0.0, )", "Monai.Deploy.InformaticsGateway.Api": "[0.4.1, )", "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )", @@ -2116,8 +2139,8 @@ "monai.deploy.informaticsgateway.database.entityframework": { "type": "Project", "dependencies": { - "Microsoft.EntityFrameworkCore": "[6.0.22, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.22, )", + "Microsoft.EntityFrameworkCore": "[6.0.25, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.25, )", "Microsoft.Extensions.Configuration.FileExtensions": "[6.0.0, )", "Microsoft.Extensions.Configuration.Json": "[6.0.0, )", "Monai.Deploy.InformaticsGateway.Api": "[0.4.1, )", @@ -2146,9 +2169,9 @@ "monai.deploy.informaticsgateway.plugins.remoteappexecution": { "type": "Project", "dependencies": { - "Microsoft.EntityFrameworkCore": "[6.0.22, )", - "Microsoft.EntityFrameworkCore.Relational": "[6.0.22, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.22, )", + "Microsoft.EntityFrameworkCore": "[6.0.25, )", + "Microsoft.EntityFrameworkCore.Relational": "[6.0.25, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.25, )", "Microsoft.Extensions.Configuration": "[6.0.1, )", "Microsoft.Extensions.Configuration.FileExtensions": "[6.0.0, )", "Microsoft.Extensions.Configuration.Json": "[6.0.0, )", diff --git a/src/InformaticsGateway/appsettings.Development.json b/src/InformaticsGateway/appsettings.Development.json old mode 100644 new mode 100755 index 544cb9858..6c6e82c55 --- a/src/InformaticsGateway/appsettings.Development.json +++ b/src/InformaticsGateway/appsettings.Development.json @@ -10,7 +10,7 @@ "InformaticsGateway": { "dicom": { "scp": { - "port": 1104, + "port": 104, "rejectUnknownSources": false } }, @@ -27,8 +27,7 @@ "username": "rabbitmq", "password": "rabbitmq", "virtualHost": "monaideploy", - "exchange": "monaideploy", - "exportRequestQueue": "export_tasks" + "exchange": "monaideploy" } }, "storage": { diff --git a/src/InformaticsGateway/appsettings.Test.json b/src/InformaticsGateway/appsettings.Test.json old mode 100644 new mode 100755 index 5f81a9e72..a2441e86c --- a/src/InformaticsGateway/appsettings.Test.json +++ b/src/InformaticsGateway/appsettings.Test.json @@ -6,6 +6,7 @@ "dicom": { "scp": { "port": 1104, + "externalAppPort": 1106, "rejectUnknownSources": false } }, @@ -23,7 +24,6 @@ "password": "rabbitmq", "virtualHost": "monaideploy", "exchange": "monaideploy", - "exportRequestQueue": "export_tasks" } }, "storage": { diff --git a/src/InformaticsGateway/appsettings.json b/src/InformaticsGateway/appsettings.json index a47b82df3..61dc5b0dc 100755 --- a/src/InformaticsGateway/appsettings.json +++ b/src/InformaticsGateway/appsettings.json @@ -57,6 +57,7 @@ "dicom": { "scp": { "port": 104, + "externalAppPort": 105, "logDimseDatasets": false, "rejectUnknownSources": true }, @@ -82,11 +83,16 @@ "password": "password", "virtualHost": "monaideploy", "exchange": "monaideploy", - "exportRequestQueue": "export_tasks", "deadLetterExchange": "monaideploy-dead-letter", "deliveryLimit": 3, "requeueDelay": 30 + }, + "topics": { + "externalAppRequest": "md.externalapp.request", + "exportHl7": "md.export.hl7", + "exportHl7Complete": "md.export.hl7complete" } + }, "storage": { "localTemporaryStoragePath": "/payloads", diff --git a/src/InformaticsGateway/packages.lock.json b/src/InformaticsGateway/packages.lock.json index 743e6d5b1..1778d145c 100755 --- a/src/InformaticsGateway/packages.lock.json +++ b/src/InformaticsGateway/packages.lock.json @@ -18,13 +18,23 @@ "resolved": "2.36.0", "contentHash": "N1HLMeIqYuY+4O69ItgZJoDBnnpNkK5N2pClceTJ2nFJxsP48iCsA4iz3tm43Yszi4r/vaThoc3UoLBfGP3vKw==" }, + "Microsoft.EntityFrameworkCore.Design": { + "type": "Direct", + "requested": "[6.0.25, )", + "resolved": "6.0.25", + "contentHash": "YawyMKj1f+GkwHrxMIf9tX84sMGgLFa5YoRmyuUugGhffiubkVLYIrlm4W0uSy2NzX4t6+V7keFLQf7lRQvDmA==", + "dependencies": { + "Humanizer.Core": "2.8.26", + "Microsoft.EntityFrameworkCore.Relational": "6.0.25" + } + }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Direct", - "requested": "[1.0.4, )", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "requested": "[1.0.5, )", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } @@ -76,16 +86,6 @@ "Swashbuckle.AspNetCore.SwaggerUI": "6.5.0" } }, - "AideDicomTools": { - "type": "Transitive", - "resolved": "0.1.1-rc0062", - "contentHash": "9m4nJ5FyKCdmj/hcnPxwKVgerZbxsBT4imyLUmfK+0S+CuRsGurXOVxH3ePKBq8tUbdzv/72pQV1ZaLa8+qj5g==", - "dependencies": { - "Microsoft.Extensions.Configuration": "6.0.0", - "MongoDB.Driver": "2.21.0", - "fo-dicom": "5.1.1" - } - }, "Ardalis.GuardClauses": { "type": "Transitive", "resolved": "4.1.1", @@ -152,6 +152,11 @@ "System.Threading.Channels": "6.0.0" } }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.8.26", + "contentHash": "OiKusGL20vby4uDEswj2IgkdchC1yQ6rwbIkZDVBPIR6al2b7n3pC91elBul9q33KaBgRKhbZH3+2Ur4fnWx2A==" + }, "Macross.Json.Extensions": { "type": "Transitive", "resolved": "3.0.0", @@ -191,19 +196,19 @@ }, "Microsoft.Data.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "gtIGHbGnRq/h4mFSJYr9BdMObvJV/a67nBubs50VjPDusQARtWJzeVTirDWsbL1qTvGzbbZCD7VE7+s2ixZfow==", + "resolved": "6.0.25", + "contentHash": "rbXNoMg/ylGyJxLcyetojuXFzvDG85M31DfFbqL8veN4P8oG6wmnPwWNn3/bDIEDVvdw15R092dxpobQeQcjGg==", "dependencies": { "SQLitePCLRaw.core": "2.1.2" } }, "Microsoft.EntityFrameworkCore": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "vNe+y8ZsEf1CsfmfYttfKAz/IgCCtphgguvao0HWNJNdjZf9cabD288nZJ17b/WaQMWXhLwYAsofk8vNVkfTOA==", + "resolved": "6.0.25", + "contentHash": "txcqw2xrmvMoTIgzAdUk8JHLELofGgTK3i6glswVZs4SC8BOU1M/iSAtwMIVtAtfzxuBIUAbHPx+Ly6lfkYe7g==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "6.0.22", - "Microsoft.EntityFrameworkCore.Analyzers": "6.0.22", + "Microsoft.EntityFrameworkCore.Abstractions": "6.0.25", + "Microsoft.EntityFrameworkCore.Analyzers": "6.0.25", "Microsoft.Extensions.Caching.Memory": "6.0.1", "Microsoft.Extensions.DependencyInjection": "6.0.1", "Microsoft.Extensions.Logging": "6.0.0", @@ -213,39 +218,39 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "82SZfdrLe7bdDB8/3INV0UULvlUzsdHkrEYylDCrzFXRWHXG9eO5jJQjRHU8j9XkGIN+MSPgIlczBnqeDvB36A==" + "resolved": "6.0.25", + "contentHash": "i6UpdWqWxSBbIFOkaMoubM40yIjTZO+0rIUkY5JRltSeFI4PzncBBQcNVNXXjAmiLXF/xY0xTS+ykClbkV46Yg==" }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "W7yfdEbEuS1OPPxU0EOA6haqI4uvzs7OwHKh81DiJFn3NFNP2ztSovkOzBDhTwHX0j+OySsAj3BEJhuzTVYIVw==", + "resolved": "6.0.25", + "contentHash": "ci2lR++x7R7LR71+HoeRnB9Z5VeOQ1ILLbFRhsjjWZyLrAMkdq7TK9Ll47jo1TXDWF8Ddeap1JgcptgPKkWSRA==", "dependencies": { - "Microsoft.EntityFrameworkCore": "6.0.22", + "Microsoft.EntityFrameworkCore": "6.0.25", "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" } }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "EDKnYZtxq7P131xxLsEokda86WnFRiVAveLVAYR8kzyWl/UwTpf/RS2m2FrbH/U8vX3A+IQNpabtxcjtCUrY0g==", + "resolved": "6.0.25", + "contentHash": "vaQNuXgUN0nIzFXQiPSb9iAaJqLvZA164Sx9mjF5rFQS5cwQ/AiymF0e4J0QH3P07Mf3zEVZE5u2fTO0NacuMQ==", "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.22", + "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.25", "SQLitePCLRaw.bundle_e_sqlite3": "2.1.2" } }, "Microsoft.EntityFrameworkCore.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "xSU77ORQgwlD+s5Cmlk9DzoSCu5oxlHLuQl+v5zAZ0Uv5yH17hp02TBfz3x9nBA+CrIsqaLjGEuyZmLDf/5ATw==", + "resolved": "6.0.25", + "contentHash": "IU4E8I9FS2sUVxJJ0w/4jogLQ8C0zvu/SO6b1tRmiiCtTrHhjUB0tqhxjrFnDXZ/mpCJOElw50+qhbcElm0CYw==", "dependencies": { - "Microsoft.Data.Sqlite.Core": "6.0.22", - "Microsoft.EntityFrameworkCore.Relational": "6.0.22", + "Microsoft.Data.Sqlite.Core": "6.0.25", + "Microsoft.EntityFrameworkCore.Relational": "6.0.25", "Microsoft.Extensions.DependencyModel": "6.0.0" } }, @@ -299,24 +304,6 @@ "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" } }, - "Microsoft.Extensions.Configuration.CommandLine": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "3nL1qCkZ1Oxx14ZTzgo4MmlO7tso7F+TtMZAY2jUAtTLyAcDp+EDjk3RqafoKiNaePyPvvlleEcBxh3b2Hzl1g==", - "dependencies": { - "Microsoft.Extensions.Configuration": "6.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" - } - }, - "Microsoft.Extensions.Configuration.EnvironmentVariables": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "pnyXV1LFOsYjGveuC07xp0YHIyGq7jRq5Ncb5zrrIieMLWVwgMyYxcOH0jTnBedDT4Gh1QinSqsjqzcieHk1og==", - "dependencies": { - "Microsoft.Extensions.Configuration": "6.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" - } - }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Transitive", "resolved": "6.0.0", @@ -341,17 +328,6 @@ "System.Text.Json": "6.0.0" } }, - "Microsoft.Extensions.Configuration.UserSecrets": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "Fy8yr4V6obi7ZxvKYI1i85jqtwMq8tqyxQVZpRSkgeA8enqy/KvBIMdcuNdznlxQMZa72mvbHqb7vbg4Pyx95w==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", - "Microsoft.Extensions.Configuration.Json": "6.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", - "Microsoft.Extensions.FileProviders.Physical": "6.0.0" - } - }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "6.0.1", @@ -380,10 +356,10 @@ }, "Microsoft.Extensions.Diagnostics.HealthChecks": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "HB1Zp1NY9m+HwYKLZBgUfNIt0xXzm4APARDuAIPODl8pT4g10oOiEDN8asOzx/sfL9xM+Sse5Zne9L+6qYi/iA==", + "resolved": "6.0.25", + "contentHash": "9vz47iGkzqhh0bGqomOTxaJNEEajeNcbSTSWwhh9Soo9lWm0UdPbw04CxXCQJPhc0aw9OaMnOxx7sCcde8/adA==", "dependencies": { - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.22", + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.25", "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", "Microsoft.Extensions.Logging.Abstractions": "6.0.4", "Microsoft.Extensions.Options": "6.0.0" @@ -391,17 +367,17 @@ }, "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "yvz+0r3qAt6gNEKlGSBO1BXMhtD3Tt8yzU59dHASolpwlSHvgqy0tEP6KXn3MPoKlPr0CiAHUdzOwYSoljzRSg==" + "resolved": "6.0.25", + "contentHash": "9sd1K/rp/vlxrBWNa0i8fgHCBPg94cocGMsJr7z9e2zQGQxMHNGpspdcy/FRGPAh2CINQet/RrM6Ef196xI20w==" }, "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "PNj+/e/GCJh3ZNzxEGhkMpKJgmmbuGar6Uk/R3mPFZacTx6lBdLs4Ev7uf4XQWqTdJe56rK+2P3oF/9jIGbxgw==", + "resolved": "6.0.25", + "contentHash": "Cmhq0sgb53+dh9xHOlBEQUhi13vsZeQ4fcYC9JYO4med7pabj9x3100opCdUv+7UX+tUC1GPm/nco+1skJdLFA==", "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "6.0.22", - "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.22", - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.22" + "Microsoft.EntityFrameworkCore.Relational": "6.0.25", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.25", + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.25" } }, "Microsoft.Extensions.FileProviders.Abstractions": { @@ -427,34 +403,6 @@ "resolved": "6.0.0", "contentHash": "ip8jnL1aPiaPeKINCqaTEbvBFDmVx9dXQEBZ2HOBRXPD1eabGNqP/bKlsIcp7U2lGxiXd5xIhoFcmY8nM4Hdiw==" }, - "Microsoft.Extensions.Hosting": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "hbmizc9KPWOacLU8Z8YMaBG6KWdZFppczYV/KwnPGU/8xebWxQxdDeJmLOgg968prb7g2oQgnp6JVLX6lgby8g==", - "dependencies": { - "Microsoft.Extensions.Configuration": "6.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", - "Microsoft.Extensions.Configuration.Binder": "6.0.0", - "Microsoft.Extensions.Configuration.CommandLine": "6.0.0", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "6.0.1", - "Microsoft.Extensions.Configuration.FileExtensions": "6.0.0", - "Microsoft.Extensions.Configuration.Json": "6.0.0", - "Microsoft.Extensions.Configuration.UserSecrets": "6.0.1", - "Microsoft.Extensions.DependencyInjection": "6.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", - "Microsoft.Extensions.FileProviders.Physical": "6.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging.Configuration": "6.0.0", - "Microsoft.Extensions.Logging.Console": "6.0.0", - "Microsoft.Extensions.Logging.Debug": "6.0.0", - "Microsoft.Extensions.Logging.EventLog": "6.0.0", - "Microsoft.Extensions.Logging.EventSource": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0" - } - }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", "resolved": "6.0.0", @@ -497,55 +445,6 @@ "Microsoft.Extensions.Options.ConfigurationExtensions": "6.0.0" } }, - "Microsoft.Extensions.Logging.Console": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "gsqKzOEdsvq28QiXFxagmn1oRB9GeI5GgYCkoybZtQA0IUb7QPwf1WmN3AwJeNIsadTvIFQCiVK0OVIgKfOBGg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging.Configuration": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0", - "System.Text.Json": "6.0.0" - } - }, - "Microsoft.Extensions.Logging.Debug": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "M9g/JixseSZATJE9tcMn9uzoD4+DbSglivFqVx8YkRJ7VVPmnvCEbOZ0AAaxsL1EKyI4cz07DXOOJExxNsUOHw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0" - } - }, - "Microsoft.Extensions.Logging.EventLog": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "rlo0RxlMd0WtLG3CHI0qOTp6fFn7MvQjlrCjucA31RqmiMFCZkF8CHNbe8O7tbBIyyoLGWB1he9CbaA5iyHthg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0", - "System.Diagnostics.EventLog": "6.0.0" - } - }, - "Microsoft.Extensions.Logging.EventSource": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "BeDyyqt7nkm/nr+Gdk+L8n1tUT/u33VkbXAOesgYSNsxDM9hJ1NOBGoZfj9rCbeD2+9myElI6JOVVFmnzgeWQA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0", - "Microsoft.Extensions.Primitives": "6.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Json": "6.0.0" - } - }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "6.0.0", @@ -662,8 +561,8 @@ }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -1072,11 +971,6 @@ "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, - "System.Diagnostics.EventLog": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" - }, "System.Diagnostics.Tools": { "type": "Transitive", "resolved": "4.3.0", @@ -1798,32 +1692,15 @@ "resolved": "0.6.2", "contentHash": "jPao/LdUNLUz8rn3H1D8W7wQbZsRZM0iayvWI4xGejJg3XJHT56gcmYdgmCGPdJF1UEBqUjucCRrFB+4HbJsbw==" }, - "monai-deploy-informatics-gateway-pseudonymisation": { - "type": "Project", - "dependencies": { - "AideDicomTools": "[0.1.1-rc0062, )", - "Ardalis.GuardClauses": "[4.1.1, )", - "Microsoft.EntityFrameworkCore": "[6.0.22, )", - "Microsoft.EntityFrameworkCore.Relational": "[6.0.22, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.22, )", - "Microsoft.Extensions.Configuration": "[6.0.0, )", - "Microsoft.Extensions.Configuration.FileExtensions": "[6.0.0, )", - "Microsoft.Extensions.Configuration.Json": "[6.0.0, )", - "Microsoft.Extensions.Hosting": "[6.0.1, )", - "MongoDB.Driver": "[2.21.0, )", - "NLog": "[5.2.3, )", - "Polly": "[7.2.4, )", - "fo-dicom": "[5.1.1, )" - } - }, "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { + "HL7-dotnetcore": "[2.36.0, )", "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.22, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.25, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[1.0.4, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging": "[1.0.5, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Storage": "[0.2.18, )", "fo-dicom": "[5.1.1, )" } @@ -1852,7 +1729,7 @@ "type": "Project", "dependencies": { "AspNetCore.HealthChecks.MongoDb": "[6.0.2, )", - "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "[6.0.22, )", + "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "[6.0.25, )", "Microsoft.Extensions.Options.ConfigurationExtensions": "[6.0.0, )", "Monai.Deploy.InformaticsGateway.Api": "[0.4.1, )", "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )", @@ -1872,8 +1749,8 @@ "monai.deploy.informaticsgateway.database.entityframework": { "type": "Project", "dependencies": { - "Microsoft.EntityFrameworkCore": "[6.0.22, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.22, )", + "Microsoft.EntityFrameworkCore": "[6.0.25, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.25, )", "Microsoft.Extensions.Configuration.FileExtensions": "[6.0.0, )", "Microsoft.Extensions.Configuration.Json": "[6.0.0, )", "Monai.Deploy.InformaticsGateway.Api": "[0.4.1, )", @@ -1902,9 +1779,9 @@ "monai.deploy.informaticsgateway.plugins.remoteappexecution": { "type": "Project", "dependencies": { - "Microsoft.EntityFrameworkCore": "[6.0.22, )", - "Microsoft.EntityFrameworkCore.Relational": "[6.0.22, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.22, )", + "Microsoft.EntityFrameworkCore": "[6.0.25, )", + "Microsoft.EntityFrameworkCore.Relational": "[6.0.25, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.25, )", "Microsoft.Extensions.Configuration": "[6.0.1, )", "Microsoft.Extensions.Configuration.FileExtensions": "[6.0.0, )", "Microsoft.Extensions.Configuration.Json": "[6.0.0, )", diff --git a/src/Plug-ins/RemoteAppExecution/DicomDeidentifier.cs b/src/Plug-ins/RemoteAppExecution/DicomDeidentifier.cs index 908da3313..19c81aa07 100755 --- a/src/Plug-ins/RemoteAppExecution/DicomDeidentifier.cs +++ b/src/Plug-ins/RemoteAppExecution/DicomDeidentifier.cs @@ -20,7 +20,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database; diff --git a/src/Plug-ins/RemoteAppExecution/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.csproj b/src/Plug-ins/RemoteAppExecution/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.csproj index bf2bc0cd2..b02fccd0a 100755 --- a/src/Plug-ins/RemoteAppExecution/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.csproj +++ b/src/Plug-ins/RemoteAppExecution/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.csproj @@ -47,13 +47,13 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/src/Plug-ins/RemoteAppExecution/RemoteAppExecution.cs b/src/Plug-ins/RemoteAppExecution/RemoteAppExecution.cs index 52f2be710..fee0722da 100755 --- a/src/Plug-ins/RemoteAppExecution/RemoteAppExecution.cs +++ b/src/Plug-ins/RemoteAppExecution/RemoteAppExecution.cs @@ -16,7 +16,7 @@ using System.Text.Json.Serialization; using FellowOakDicom; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution { diff --git a/src/Plug-ins/RemoteAppExecution/Test/DicomDeidentifierTest.cs b/src/Plug-ins/RemoteAppExecution/Test/DicomDeidentifierTest.cs index 285771f4a..c1e458cf9 100755 --- a/src/Plug-ins/RemoteAppExecution/Test/DicomDeidentifierTest.cs +++ b/src/Plug-ins/RemoteAppExecution/Test/DicomDeidentifierTest.cs @@ -19,7 +19,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database; diff --git a/src/Plug-ins/RemoteAppExecution/Test/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.csproj b/src/Plug-ins/RemoteAppExecution/Test/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.csproj old mode 100644 new mode 100755 index a1070e061..22f46686c --- a/src/Plug-ins/RemoteAppExecution/Test/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.csproj +++ b/src/Plug-ins/RemoteAppExecution/Test/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.csproj @@ -43,9 +43,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + diff --git a/src/Plug-ins/RemoteAppExecution/Test/packages.lock.json b/src/Plug-ins/RemoteAppExecution/Test/packages.lock.json index d16f2b9c1..ac62dee73 100755 --- a/src/Plug-ins/RemoteAppExecution/Test/packages.lock.json +++ b/src/Plug-ins/RemoteAppExecution/Test/packages.lock.json @@ -10,31 +10,31 @@ }, "Microsoft.EntityFrameworkCore.InMemory": { "type": "Direct", - "requested": "[6.0.22, )", - "resolved": "6.0.22", - "contentHash": "CcL5ajX+/OkafcP5OMplCBnIgSfaQy5BUjEZQKZ9BlspnwFFucy+wcE0LL1ycOlWcDYGI42FnQ45dD1Kcz+ZKA==", + "requested": "[6.0.25, )", + "resolved": "6.0.25", + "contentHash": "T1wFaHL0WS51PlrSzWfBX2qppMbuIserPUaSwrw6Uhvg4WllsQPKYqFGAZC9bbUAihjgY5es7MIgSEtXYNdLiw==", "dependencies": { - "Microsoft.EntityFrameworkCore": "6.0.22" + "Microsoft.EntityFrameworkCore": "6.0.25" } }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "Direct", - "requested": "[6.0.22, )", - "resolved": "6.0.22", - "contentHash": "EDKnYZtxq7P131xxLsEokda86WnFRiVAveLVAYR8kzyWl/UwTpf/RS2m2FrbH/U8vX3A+IQNpabtxcjtCUrY0g==", + "requested": "[6.0.25, )", + "resolved": "6.0.25", + "contentHash": "vaQNuXgUN0nIzFXQiPSb9iAaJqLvZA164Sx9mjF5rFQS5cwQ/AiymF0e4J0QH3P07Mf3zEVZE5u2fTO0NacuMQ==", "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.22", + "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.25", "SQLitePCLRaw.bundle_e_sqlite3": "2.1.2" } }, "Microsoft.EntityFrameworkCore.Sqlite.Core": { "type": "Direct", - "requested": "[6.0.22, )", - "resolved": "6.0.22", - "contentHash": "xSU77ORQgwlD+s5Cmlk9DzoSCu5oxlHLuQl+v5zAZ0Uv5yH17hp02TBfz3x9nBA+CrIsqaLjGEuyZmLDf/5ATw==", + "requested": "[6.0.25, )", + "resolved": "6.0.25", + "contentHash": "IU4E8I9FS2sUVxJJ0w/4jogLQ8C0zvu/SO6b1tRmiiCtTrHhjUB0tqhxjrFnDXZ/mpCJOElw50+qhbcElm0CYw==", "dependencies": { - "Microsoft.Data.Sqlite.Core": "6.0.22", - "Microsoft.EntityFrameworkCore.Relational": "6.0.22", + "Microsoft.Data.Sqlite.Core": "6.0.25", + "Microsoft.EntityFrameworkCore.Relational": "6.0.25", "Microsoft.Extensions.DependencyModel": "6.0.0" } }, @@ -149,6 +149,11 @@ "System.Threading.Channels": "6.0.0" } }, + "HL7-dotnetcore": { + "type": "Transitive", + "resolved": "2.36.0", + "contentHash": "N1HLMeIqYuY+4O69ItgZJoDBnnpNkK5N2pClceTJ2nFJxsP48iCsA4iz3tm43Yszi4r/vaThoc3UoLBfGP3vKw==" + }, "Macross.Json.Extensions": { "type": "Transitive", "resolved": "3.0.0", @@ -171,19 +176,19 @@ }, "Microsoft.Data.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "gtIGHbGnRq/h4mFSJYr9BdMObvJV/a67nBubs50VjPDusQARtWJzeVTirDWsbL1qTvGzbbZCD7VE7+s2ixZfow==", + "resolved": "6.0.25", + "contentHash": "rbXNoMg/ylGyJxLcyetojuXFzvDG85M31DfFbqL8veN4P8oG6wmnPwWNn3/bDIEDVvdw15R092dxpobQeQcjGg==", "dependencies": { "SQLitePCLRaw.core": "2.1.2" } }, "Microsoft.EntityFrameworkCore": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "vNe+y8ZsEf1CsfmfYttfKAz/IgCCtphgguvao0HWNJNdjZf9cabD288nZJ17b/WaQMWXhLwYAsofk8vNVkfTOA==", + "resolved": "6.0.25", + "contentHash": "txcqw2xrmvMoTIgzAdUk8JHLELofGgTK3i6glswVZs4SC8BOU1M/iSAtwMIVtAtfzxuBIUAbHPx+Ly6lfkYe7g==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "6.0.22", - "Microsoft.EntityFrameworkCore.Analyzers": "6.0.22", + "Microsoft.EntityFrameworkCore.Abstractions": "6.0.25", + "Microsoft.EntityFrameworkCore.Analyzers": "6.0.25", "Microsoft.Extensions.Caching.Memory": "6.0.1", "Microsoft.Extensions.DependencyInjection": "6.0.1", "Microsoft.Extensions.Logging": "6.0.0", @@ -193,20 +198,20 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "82SZfdrLe7bdDB8/3INV0UULvlUzsdHkrEYylDCrzFXRWHXG9eO5jJQjRHU8j9XkGIN+MSPgIlczBnqeDvB36A==" + "resolved": "6.0.25", + "contentHash": "i6UpdWqWxSBbIFOkaMoubM40yIjTZO+0rIUkY5JRltSeFI4PzncBBQcNVNXXjAmiLXF/xY0xTS+ykClbkV46Yg==" }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "W7yfdEbEuS1OPPxU0EOA6haqI4uvzs7OwHKh81DiJFn3NFNP2ztSovkOzBDhTwHX0j+OySsAj3BEJhuzTVYIVw==", + "resolved": "6.0.25", + "contentHash": "ci2lR++x7R7LR71+HoeRnB9Z5VeOQ1ILLbFRhsjjWZyLrAMkdq7TK9Ll47jo1TXDWF8Ddeap1JgcptgPKkWSRA==", "dependencies": { - "Microsoft.EntityFrameworkCore": "6.0.22", + "Microsoft.EntityFrameworkCore": "6.0.25", "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" } }, @@ -449,8 +454,8 @@ }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -460,10 +465,10 @@ }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } @@ -1595,11 +1600,12 @@ "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { + "HL7-dotnetcore": "[2.36.0, )", "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.22, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.25, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[1.0.4, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging": "[1.0.5, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Storage": "[0.2.18, )", "fo-dicom": "[5.1.1, )" } @@ -1629,9 +1635,9 @@ "monai.deploy.informaticsgateway.plugins.remoteappexecution": { "type": "Project", "dependencies": { - "Microsoft.EntityFrameworkCore": "[6.0.22, )", - "Microsoft.EntityFrameworkCore.Relational": "[6.0.22, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.22, )", + "Microsoft.EntityFrameworkCore": "[6.0.25, )", + "Microsoft.EntityFrameworkCore.Relational": "[6.0.25, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.25, )", "Microsoft.Extensions.Configuration": "[6.0.1, )", "Microsoft.Extensions.Configuration.FileExtensions": "[6.0.0, )", "Microsoft.Extensions.Configuration.Json": "[6.0.0, )", diff --git a/src/Plug-ins/RemoteAppExecution/packages.lock.json b/src/Plug-ins/RemoteAppExecution/packages.lock.json index 439940cdf..a171e527a 100755 --- a/src/Plug-ins/RemoteAppExecution/packages.lock.json +++ b/src/Plug-ins/RemoteAppExecution/packages.lock.json @@ -4,12 +4,12 @@ "net6.0": { "Microsoft.EntityFrameworkCore": { "type": "Direct", - "requested": "[6.0.22, )", - "resolved": "6.0.22", - "contentHash": "vNe+y8ZsEf1CsfmfYttfKAz/IgCCtphgguvao0HWNJNdjZf9cabD288nZJ17b/WaQMWXhLwYAsofk8vNVkfTOA==", + "requested": "[6.0.25, )", + "resolved": "6.0.25", + "contentHash": "txcqw2xrmvMoTIgzAdUk8JHLELofGgTK3i6glswVZs4SC8BOU1M/iSAtwMIVtAtfzxuBIUAbHPx+Ly6lfkYe7g==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "6.0.22", - "Microsoft.EntityFrameworkCore.Analyzers": "6.0.22", + "Microsoft.EntityFrameworkCore.Abstractions": "6.0.25", + "Microsoft.EntityFrameworkCore.Analyzers": "6.0.25", "Microsoft.Extensions.Caching.Memory": "6.0.1", "Microsoft.Extensions.DependencyInjection": "6.0.1", "Microsoft.Extensions.Logging": "6.0.0", @@ -19,31 +19,31 @@ }, "Microsoft.EntityFrameworkCore.Design": { "type": "Direct", - "requested": "[6.0.22, )", - "resolved": "6.0.22", - "contentHash": "es9TKd0cpM263Ou0QMEETN7MDzD7kXDkThiiXl1+c/69v97AZlzeLoM5tDdC0RC4L74ZWyk3+WMnoDPL93DDyQ==", + "requested": "[6.0.25, )", + "resolved": "6.0.25", + "contentHash": "YawyMKj1f+GkwHrxMIf9tX84sMGgLFa5YoRmyuUugGhffiubkVLYIrlm4W0uSy2NzX4t6+V7keFLQf7lRQvDmA==", "dependencies": { "Humanizer.Core": "2.8.26", - "Microsoft.EntityFrameworkCore.Relational": "6.0.22" + "Microsoft.EntityFrameworkCore.Relational": "6.0.25" } }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Direct", - "requested": "[6.0.22, )", - "resolved": "6.0.22", - "contentHash": "W7yfdEbEuS1OPPxU0EOA6haqI4uvzs7OwHKh81DiJFn3NFNP2ztSovkOzBDhTwHX0j+OySsAj3BEJhuzTVYIVw==", + "requested": "[6.0.25, )", + "resolved": "6.0.25", + "contentHash": "ci2lR++x7R7LR71+HoeRnB9Z5VeOQ1ILLbFRhsjjWZyLrAMkdq7TK9Ll47jo1TXDWF8Ddeap1JgcptgPKkWSRA==", "dependencies": { - "Microsoft.EntityFrameworkCore": "6.0.22", + "Microsoft.EntityFrameworkCore": "6.0.25", "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" } }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "Direct", - "requested": "[6.0.22, )", - "resolved": "6.0.22", - "contentHash": "EDKnYZtxq7P131xxLsEokda86WnFRiVAveLVAYR8kzyWl/UwTpf/RS2m2FrbH/U8vX3A+IQNpabtxcjtCUrY0g==", + "requested": "[6.0.25, )", + "resolved": "6.0.25", + "contentHash": "vaQNuXgUN0nIzFXQiPSb9iAaJqLvZA164Sx9mjF5rFQS5cwQ/AiymF0e4J0QH3P07Mf3zEVZE5u2fTO0NacuMQ==", "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.22", + "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.25", "SQLitePCLRaw.bundle_e_sqlite3": "2.1.2" } }, @@ -169,6 +169,11 @@ "System.Threading.Channels": "6.0.0" } }, + "HL7-dotnetcore": { + "type": "Transitive", + "resolved": "2.36.0", + "contentHash": "N1HLMeIqYuY+4O69ItgZJoDBnnpNkK5N2pClceTJ2nFJxsP48iCsA4iz3tm43Yszi4r/vaThoc3UoLBfGP3vKw==" + }, "Humanizer.Core": { "type": "Transitive", "resolved": "2.8.26", @@ -191,29 +196,29 @@ }, "Microsoft.Data.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "gtIGHbGnRq/h4mFSJYr9BdMObvJV/a67nBubs50VjPDusQARtWJzeVTirDWsbL1qTvGzbbZCD7VE7+s2ixZfow==", + "resolved": "6.0.25", + "contentHash": "rbXNoMg/ylGyJxLcyetojuXFzvDG85M31DfFbqL8veN4P8oG6wmnPwWNn3/bDIEDVvdw15R092dxpobQeQcjGg==", "dependencies": { "SQLitePCLRaw.core": "2.1.2" } }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "82SZfdrLe7bdDB8/3INV0UULvlUzsdHkrEYylDCrzFXRWHXG9eO5jJQjRHU8j9XkGIN+MSPgIlczBnqeDvB36A==" + "resolved": "6.0.25", + "contentHash": "i6UpdWqWxSBbIFOkaMoubM40yIjTZO+0rIUkY5JRltSeFI4PzncBBQcNVNXXjAmiLXF/xY0xTS+ykClbkV46Yg==" }, "Microsoft.EntityFrameworkCore.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "xSU77ORQgwlD+s5Cmlk9DzoSCu5oxlHLuQl+v5zAZ0Uv5yH17hp02TBfz3x9nBA+CrIsqaLjGEuyZmLDf/5ATw==", + "resolved": "6.0.25", + "contentHash": "IU4E8I9FS2sUVxJJ0w/4jogLQ8C0zvu/SO6b1tRmiiCtTrHhjUB0tqhxjrFnDXZ/mpCJOElw50+qhbcElm0CYw==", "dependencies": { - "Microsoft.Data.Sqlite.Core": "6.0.22", - "Microsoft.EntityFrameworkCore.Relational": "6.0.22", + "Microsoft.Data.Sqlite.Core": "6.0.25", + "Microsoft.EntityFrameworkCore.Relational": "6.0.25", "Microsoft.Extensions.DependencyModel": "6.0.0" } }, @@ -378,8 +383,8 @@ }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -389,10 +394,10 @@ }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } @@ -589,11 +594,12 @@ "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { + "HL7-dotnetcore": "[2.36.0, )", "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.22, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.25, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[1.0.4, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging": "[1.0.5, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Storage": "[0.2.18, )", "fo-dicom": "[5.1.1, )" } diff --git a/tests/Integration.Test/Common/Assertions.cs b/tests/Integration.Test/Common/Assertions.cs old mode 100644 new mode 100755 index 09d7eeb5f..f0d861570 --- a/tests/Integration.Test/Common/Assertions.cs +++ b/tests/Integration.Test/Common/Assertions.cs @@ -34,7 +34,7 @@ namespace Monai.Deploy.InformaticsGateway.Integration.Test.Common { - internal class Assertions + public class Assertions { private readonly Configurations _configurations; private readonly InformaticsGatewayConfiguration _options; @@ -401,5 +401,10 @@ private void CompareDicomFiles(DicomFile left, DicomFile right, DicomTag[] dicom left.Dataset.GetString(tag).Should().Be(right.Dataset.GetString(tag)); } } + + public static void ShouldBeInMessageDictionary(Dictionary messages, HL7.Dotnetcore.Message message) + { + messages.Values.FirstOrDefault(m => m.HL7Message == message.HL7Message).Should().NotBeNull(); + } } } diff --git a/tests/Integration.Test/Common/DataProvider.cs b/tests/Integration.Test/Common/DataProvider.cs old mode 100644 new mode 100755 diff --git a/tests/Integration.Test/Common/DicomCEchoDataClient.cs b/tests/Integration.Test/Common/DicomCEchoDataClient.cs old mode 100644 new mode 100755 index 49918c0e2..645224589 --- a/tests/Integration.Test/Common/DicomCEchoDataClient.cs +++ b/tests/Integration.Test/Common/DicomCEchoDataClient.cs @@ -71,5 +71,6 @@ public async Task SendAsync(DataProvider dataProvider, params object[] args) dataProvider.DimseRsponse = DicomStatus.Cancel; } } + public Task SaveHl7Async(DataProvider dataProvider, params object[] args) => throw new NotImplementedException(); } } diff --git a/tests/Integration.Test/Common/DicomCStoreDataClient.cs b/tests/Integration.Test/Common/DicomCStoreDataClient.cs old mode 100644 new mode 100755 index bd010a8d0..bda321154 --- a/tests/Integration.Test/Common/DicomCStoreDataClient.cs +++ b/tests/Integration.Test/Common/DicomCStoreDataClient.cs @@ -115,5 +115,6 @@ private async Task SendBatchAsync(List files, string callingAeTitle, await dicomClient.SendAsync(); countdownEvent.Wait(timeout); } + public Task SaveHl7Async(DataProvider dataProvider, params object[] args) => throw new NotImplementedException(); } } diff --git a/tests/Integration.Test/Common/DicomWebDataSink.cs b/tests/Integration.Test/Common/DicomWebDataSink.cs old mode 100644 new mode 100755 index 07ad2ee76..fb1bf9644 --- a/tests/Integration.Test/Common/DicomWebDataSink.cs +++ b/tests/Integration.Test/Common/DicomWebDataSink.cs @@ -81,5 +81,6 @@ public async Task SendAsync(DataProvider dataProvider, params object[] args) stopwatch.Stop(); _outputHelper.WriteLine($"Time to upload to DICOMWeb={0}s...", stopwatch.Elapsed.TotalSeconds); } + public Task SaveHl7Async(DataProvider dataProvider, params object[] args) => throw new NotImplementedException(); } } diff --git a/tests/Integration.Test/Common/FhirDataSink.cs b/tests/Integration.Test/Common/FhirDataSink.cs old mode 100644 new mode 100755 index 5db77db96..931442e50 --- a/tests/Integration.Test/Common/FhirDataSink.cs +++ b/tests/Integration.Test/Common/FhirDataSink.cs @@ -59,5 +59,6 @@ public async Task SendAsync(DataProvider dataProvider, params object[] args) stopwatch.Stop(); _outputHelper.WriteLine($"Time to upload FHIR data={0}s...", stopwatch.Elapsed.TotalSeconds); } + public Task SaveHl7Async(DataProvider dataProvider, params object[] args) => throw new NotImplementedException(); } } diff --git a/tests/Integration.Test/Common/Hl7DataSink.cs b/tests/Integration.Test/Common/Hl7DataSink.cs old mode 100644 new mode 100755 index 0c340e686..65fc0000c --- a/tests/Integration.Test/Common/Hl7DataSink.cs +++ b/tests/Integration.Test/Common/Hl7DataSink.cs @@ -140,5 +140,6 @@ private async Task SendBatchAsync(DataProvider dataProvider, params object[] arg stopwatch.Stop(); _outputHelper.WriteLine($"Took {stopwatch.Elapsed.TotalSeconds}s to send {messages.Count} messages."); } + public Task SaveHl7Async(DataProvider dataProvider, params object[] args) => throw new NotImplementedException(); } } diff --git a/tests/Integration.Test/Common/IDataClient.cs b/tests/Integration.Test/Common/IDataClient.cs old mode 100644 new mode 100755 index 254572b93..07ff97069 --- a/tests/Integration.Test/Common/IDataClient.cs +++ b/tests/Integration.Test/Common/IDataClient.cs @@ -19,5 +19,7 @@ namespace Monai.Deploy.InformaticsGateway.Integration.Test.Common internal interface IDataClient { Task SendAsync(DataProvider dataProvider, params object[] args); + + Task SaveHl7Async(DataProvider dataProvider, params object[] args); } } diff --git a/tests/Integration.Test/Common/MinioDataSink.cs b/tests/Integration.Test/Common/MinioDataSink.cs old mode 100644 new mode 100755 index dde07d4a1..f54d61f2e --- a/tests/Integration.Test/Common/MinioDataSink.cs +++ b/tests/Integration.Test/Common/MinioDataSink.cs @@ -15,6 +15,7 @@ */ using System.Diagnostics; +using System.Text; using Minio; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Integration.Test.Drivers; @@ -69,6 +70,38 @@ await _retryPolicy.ExecuteAsync(async () => }); } + public async Task SaveHl7Async(DataProvider dataProvider, params object[] args) + { + await _retryPolicy.ExecuteAsync(async () => + { + var minioClient = CreateMinioClient(); + + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + _outputHelper.WriteLine($"Uploading {dataProvider.HL7Specs.Files.Count} files to MinIO..."); + + foreach (var key in dataProvider.HL7Specs.Files.Keys) + { + var file = dataProvider.HL7Specs.Files[key]; + var filename = $"{args[0]}/{key.Replace(".txt", ".hl7")}"; + var byteArray = Encoding.ASCII.GetBytes(file.HL7Message); + var stream = new MemoryStream(byteArray); + + stream.Position = 0; + var puObjectArgs = new PutObjectArgs(); + puObjectArgs.WithBucket(_options.Storage.StorageServiceBucketName) + .WithObject(filename) + .WithStreamData(stream) + .WithObjectSize(stream.Length); + await minioClient.PutObjectAsync(puObjectArgs); + } + + stopwatch.Stop(); + _outputHelper.WriteLine($"Time to upload to Minio={0}s...", stopwatch.Elapsed.TotalSeconds); + }); + } + private MinioClient CreateMinioClient() => new MinioClient() .WithEndpoint(_options.Storage.Settings["endpoint"]) .WithCredentials(_options.Storage.Settings["accessKey"], _options.Storage.Settings["accessToken"]) diff --git a/tests/Integration.Test/Drivers/MongoDBDataProvider.cs b/tests/Integration.Test/Drivers/MongoDBDataProvider.cs old mode 100644 new mode 100755 index 2c86182b7..cf1ac780c --- a/tests/Integration.Test/Drivers/MongoDBDataProvider.cs +++ b/tests/Integration.Test/Drivers/MongoDBDataProvider.cs @@ -15,6 +15,7 @@ */ using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Api.Rest; using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Database.Api; diff --git a/tests/Integration.Test/Features/ExternalApp.feature b/tests/Integration.Test/Features/ExternalApp.feature new file mode 100755 index 000000000..831f0c85d --- /dev/null +++ b/tests/Integration.Test/Features/ExternalApp.feature @@ -0,0 +1,27 @@ + +# Copyright 2023 MONAI Consortium +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +# @ignored + +Feature: External App Execution + +This feature tests the External App Execution for saving and +re-identifying data sent and received by the MIG respectively. + + @messaging_workflow_request @messaging + Scenario: End-to-end test of external App scp incomming + Given a externalApp study that is exported to the test host + When the externalApp study is received and sent back to Informatics Gateway with 1 message + Then ensure the original externalApp study and the received study are the same diff --git a/tests/Integration.Test/Features/HL7Export.feature b/tests/Integration.Test/Features/HL7Export.feature new file mode 100755 index 000000000..b40748c8e --- /dev/null +++ b/tests/Integration.Test/Features/HL7Export.feature @@ -0,0 +1,32 @@ + +# Copyright 2023 MONAI Consortium +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. + +# @ignored + +Feature: HL7 Export + +This feature tests the Export Hl7. + + @messaging_workflow_request @messaging + Scenario: End-to-end test of HL7 exporting + Given a HL7 message that is exported to the test host + When the HL7 Export message is received with 6 messages acked true + Then ensure that exportcomplete messages are sent with success + + + Scenario: End-to-end test of HL7 exporting with no Ack + Given a HL7 message that is exported to the test host + When the HL7 Export message is received with 6 messages acked false + Then ensure that exportcomplete messages are sent with failure diff --git a/tests/Integration.Test/Features/RemoteAppExecutionPlugIn.feature b/tests/Integration.Test/Features/RemoteAppExecutionPlugIn.feature index 00f91acb8..469408574 100755 --- a/tests/Integration.Test/Features/RemoteAppExecutionPlugIn.feature +++ b/tests/Integration.Test/Features/RemoteAppExecutionPlugIn.feature @@ -31,3 +31,4 @@ re-identifying data sent and received by the MIG respectively. Given a study that is exported to the test host with a bad plugin When the study is received and sent back to Informatics Gateway with 2 messages Then ensure the original study and the received study are the same + diff --git a/tests/Integration.Test/Hooks/TestHooks.cs b/tests/Integration.Test/Hooks/TestHooks.cs index 0b9c2f48a..6f1d24e3b 100755 --- a/tests/Integration.Test/Hooks/TestHooks.cs +++ b/tests/Integration.Test/Hooks/TestHooks.cs @@ -42,6 +42,7 @@ public sealed class TestHooks private static RabbitMqConsumer s_rabbitMqConsumer_WorkflowRequest; private static RabbitMqConsumer s_rabbitMqConsumer_ArtifactRecieved; private static RabbitMqConsumer s_rabbitMqConsumer_ExportComplete; + private static RabbitMqConsumer s_rabbitMqConsumer_ExportHL7Complete; private static IDatabaseDataProvider s_database; private static DicomScp s_dicomServer; private static DataProvider s_dataProvider; @@ -131,12 +132,20 @@ private static void SetupRabbitMq(ISpecFlowOutputHelper outputHelper, IServiceSc s_rabbitMqConsumer_ExportComplete = new RabbitMqConsumer(rabbitMqSubscriber_ExportComplete, s_options.Value.Messaging.Topics.ExportComplete, outputHelper); + var rabbitMqSubscriber_ExportHL7Complete = new RabbitMQMessageSubscriberService( + Options.Create(s_options.Value.Messaging), + scope.ServiceProvider.GetRequiredService>(), + s_rabbitMqConnectionFactory); + + s_rabbitMqConsumer_ExportHL7Complete = new RabbitMqConsumer(rabbitMqSubscriber_ExportComplete, s_options.Value.Messaging.Topics.ExportHl7Complete, outputHelper); + var rabbitMqSubscriber_ArtifactRecieved = new RabbitMQMessageSubscriberService( Options.Create(s_options.Value.Messaging), scope.ServiceProvider.GetRequiredService>(), s_rabbitMqConnectionFactory); s_rabbitMqConsumer_ArtifactRecieved = new RabbitMqConsumer(rabbitMqSubscriber_ArtifactRecieved, s_options.Value.Messaging.Topics.ArtifactRecieved, outputHelper); + } private static IDatabaseDataProvider GetDatabase(IServiceProvider serviceProvider, ISpecFlowOutputHelper outputHelper) @@ -173,6 +182,7 @@ public void SetUp(ScenarioContext scenarioContext, ISpecFlowOutputHelper outputH _objectContainer.RegisterInstanceAs(s_rabbitMqConsumer_WorkflowRequest, "WorkflowRequestSubscriber"); _objectContainer.RegisterInstanceAs(s_rabbitMqConsumer_ExportComplete, "ExportCompleteSubscriber"); _objectContainer.RegisterInstanceAs(s_rabbitMqConsumer_ArtifactRecieved, "ArtifactRecievedSubscriber"); + _objectContainer.RegisterInstanceAs(s_rabbitMqConsumer_ExportHL7Complete, "ExportHL7CompleteSubscriber"); _objectContainer.RegisterInstanceAs(s_dataProvider, "DataProvider"); _objectContainer.RegisterInstanceAs(s_assertions, "Assertions"); _objectContainer.RegisterInstanceAs(s_storescu, "StoreSCU"); @@ -192,6 +202,7 @@ public static void Shtudown() s_rabbitMqConsumer_WorkflowRequest.Dispose(); s_rabbitMqConsumer_ExportComplete.Dispose(); + s_rabbitMqConsumer_ExportHL7Complete.Dispose(); s_rabbitMqConsumer_ArtifactRecieved.Dispose(); s_rabbitMqConnectionFactory.Dispose(); } diff --git a/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj b/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj index ca7854f19..2d79b05c2 100755 --- a/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj +++ b/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj @@ -30,15 +30,15 @@ - - + + - + diff --git a/tests/Integration.Test/StepDefinitions/DicomDimseScpServicesStepDefinitions.cs b/tests/Integration.Test/StepDefinitions/DicomDimseScpServicesStepDefinitions.cs index 739947582..7bad77fa8 100755 --- a/tests/Integration.Test/StepDefinitions/DicomDimseScpServicesStepDefinitions.cs +++ b/tests/Integration.Test/StepDefinitions/DicomDimseScpServicesStepDefinitions.cs @@ -19,6 +19,7 @@ using BoDi; using FellowOakDicom.Network; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Client; using Monai.Deploy.InformaticsGateway.Client.Common; using Monai.Deploy.InformaticsGateway.Configuration; diff --git a/tests/Integration.Test/StepDefinitions/ExportServicesStepDefinitions.cs b/tests/Integration.Test/StepDefinitions/ExportServicesStepDefinitions.cs old mode 100644 new mode 100755 index 8ca5811f9..53cae4359 --- a/tests/Integration.Test/StepDefinitions/ExportServicesStepDefinitions.cs +++ b/tests/Integration.Test/StepDefinitions/ExportServicesStepDefinitions.cs @@ -18,7 +18,7 @@ using System.Net.Http.Headers; using Ardalis.GuardClauses; using BoDi; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Client; using Monai.Deploy.InformaticsGateway.Client.Common; using Monai.Deploy.InformaticsGateway.Configuration; diff --git a/tests/Integration.Test/StepDefinitions/ExteralAppStepDefinitions.cs b/tests/Integration.Test/StepDefinitions/ExteralAppStepDefinitions.cs new file mode 100755 index 000000000..168155f11 --- /dev/null +++ b/tests/Integration.Test/StepDefinitions/ExteralAppStepDefinitions.cs @@ -0,0 +1,258 @@ +/* + * Copyright 2022-2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.Net; +using BoDi; +using FellowOakDicom; +using FellowOakDicom.Network; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; +using Monai.Deploy.InformaticsGateway.Client; +using Monai.Deploy.InformaticsGateway.Client.Common; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Integration.Test.Common; +using Monai.Deploy.InformaticsGateway.Integration.Test.Drivers; +using Monai.Deploy.Messaging.Events; +using Monai.Deploy.Messaging.Messages; +using Monai.Deploy.Messaging.RabbitMQ; +using Polly; +using Polly.Timeout; + +namespace Monai.Deploy.InformaticsGateway.Integration.Test.StepDefinitions +{ + [Binding] + [CollectionDefinition("SpecFlowNonParallelizableFeatures", DisableParallelization = true)] + public class ExteralAppStepDefinitions + { + private static readonly TimeSpan MessageWaitTimeSpan = TimeSpan.FromMinutes(3); + private static readonly TimeSpan DicomScpWaitTimeSpan = TimeSpan.FromMinutes(20); + private static readonly string MonaiAeTitle = "REMOTE-APPS"; + private static readonly string SourceAeTitle = "MIGTestHost"; + private static readonly DicomTag[] DicomTags = new[] { DicomTag.AccessionNumber, DicomTag.StudyDescription, DicomTag.SeriesDescription, DicomTag.PatientAddress, DicomTag.PatientAge, DicomTag.PatientName }; + private static readonly List DefaultDicomTags = new() { DicomTag.PatientID, DicomTag.StudyInstanceUID, DicomTag.SeriesInstanceUID, DicomTag.SOPInstanceUID }; + + private readonly ObjectContainer _objectContainer; + private readonly InformaticsGatewayClient _informaticsGatewayClient; + private readonly IDataClient _dataSinkMinio; + private readonly DicomScp _dicomServer; + private readonly Configurations _configuration; + private string _dicomDestination; + private readonly DataProvider _dataProvider; + private readonly RabbitMqConsumer _receivedExportCompletedMessages; + private readonly RabbitMqConsumer _receivedWorkflowRequestMessages; + private readonly RabbitMqConsumer _receivedArtifactRecievedMessages; + private readonly RabbitMQMessagePublisherService _messagePublisher; + private readonly InformaticsGatewayConfiguration _informaticsGatewayConfiguration; + private Dictionary _originalDicomFiles; + private ExternalAppRequestEvent _exportRequestEvent; + private readonly Assertions _assertions; + private readonly string _correlationId = Guid.NewGuid().ToString(); + private readonly string _exportTaskId = Guid.NewGuid().ToString(); + private readonly string _workflowInstanceId = Guid.NewGuid().ToString(); + + public ExteralAppStepDefinitions( + ObjectContainer objectContainer, + Configurations configuration) + { + _objectContainer = objectContainer ?? throw new ArgumentNullException(nameof(objectContainer)); + _informaticsGatewayClient = objectContainer.Resolve("InformaticsGatewayClient"); + _dataSinkMinio = objectContainer.Resolve("MinioClient"); + _dicomServer = objectContainer.Resolve("DicomScp"); + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _dataProvider = objectContainer.Resolve("DataProvider"); + _receivedExportCompletedMessages = objectContainer.Resolve("ExportCompleteSubscriber"); + _receivedWorkflowRequestMessages = objectContainer.Resolve("WorkflowRequestSubscriber"); + _receivedArtifactRecievedMessages = objectContainer.Resolve("ArtifactRecievedSubscriber"); + _messagePublisher = objectContainer.Resolve("MessagingPublisher"); + _informaticsGatewayConfiguration = objectContainer.Resolve("InformaticsGatewayConfiguration"); + _assertions = objectContainer.Resolve("Assertions"); + + DefaultDicomTags.AddRange(DicomTags); + _dicomServer.ClearFilesAndUseHashes = false; //we need to store actual files to send the data back to MIG + } + + [Given(@"a externalApp study that is exported to the test host")] + public async Task GivenAExternalAppStudyThatIsExportedToTheTestHost() + { + DestinationApplicationEntity destination; + try + { + destination = await _informaticsGatewayClient.DicomDestinations.Create(new DestinationApplicationEntity + { + Name = _dicomServer.FeatureScpAeTitle, + AeTitle = _dicomServer.FeatureScpAeTitle, + HostIp = _configuration.InformaticsGatewayOptions.Host, + Port = _dicomServer.FeatureScpPort + }, CancellationToken.None); + } + catch (ProblemException ex) + { + if (ex.ProblemDetails.Status == (int)HttpStatusCode.Conflict && ex.ProblemDetails.Detail.Contains("already exists")) + { + destination = await _informaticsGatewayClient.DicomDestinations.GetAeTitle(_dicomServer.FeatureScpAeTitle, CancellationToken.None); + } + else + { + throw; + } + } + _dicomDestination = destination.Name; + + // Generate a study with multiple series + //_dataProvider.GenerateDicomData("MG", 1, 1); + _dataProvider.GenerateDicomData("CT", 1); + _dataProvider.InjectRandomData(DicomTags); + _originalDicomFiles = new Dictionary(_dataProvider.DicomSpecs.Files); + + await _dataSinkMinio.SendAsync(_dataProvider); + + // Emit a export request event + _exportRequestEvent = new ExternalAppRequestEvent + { + CorrelationId = _correlationId, + Targets = new List { new DataOrigin { Destination = destination.Name } }, + ExportTaskId = _exportTaskId, + Files = _dataProvider.DicomSpecs.Files.Keys.ToList(), + MessageId = Guid.NewGuid().ToString(), + WorkflowInstanceId = _workflowInstanceId, + DestinationFolder = "ThisIs/My/Output/Folder", + }; + + //_exportRequestEvent.PluginAssemblies.Add(typeof(DicomDeidentifier).AssemblyQualifiedName); + + var message = new JsonMessage( + _exportRequestEvent, + MessageBrokerConfiguration.InformaticsGatewayApplicationId, + _exportRequestEvent.CorrelationId, + string.Empty); + + _receivedExportCompletedMessages.ClearMessages(); + _receivedArtifactRecievedMessages.ClearMessages(); + await _messagePublisher.Publish("md.externalapp.request", message.ToMessage()); + } + + [When(@"the externalApp study is received and sent back to Informatics Gateway with (.*) message")] + public async Task WhenTheExternalAppStudyIsReceivedAndSentBackToInformaticsGatewayWithMessage(int exportCount) + { + // setup DICOM Source + try + { + await _informaticsGatewayClient.DicomSources.Create(new SourceApplicationEntity + { + Name = SourceAeTitle, + AeTitle = SourceAeTitle, + HostIp = _configuration.InformaticsGatewayOptions.Host, + }, CancellationToken.None); + _dataProvider.Source = SourceAeTitle; + } + catch (ProblemException ex) + { + if (ex.ProblemDetails.Status == (int)HttpStatusCode.Conflict && + ex.ProblemDetails.Detail.Contains("already exists")) + { + await _informaticsGatewayClient.DicomSources.GetAeTitle(SourceAeTitle, CancellationToken.None); + } + else + { + throw; + } + } + + // setup MONAI Deploy AET + _dataProvider.StudyGrouping = "0020,000D"; + try + { + await _informaticsGatewayClient.MonaiScpAeTitle.Create(new MonaiApplicationEntity + { + AeTitle = MonaiAeTitle, + Name = MonaiAeTitle, + Grouping = _dataProvider.StudyGrouping, + Timeout = 3, + PlugInAssemblies = new List() + }, CancellationToken.None); + _dataProvider.Destination = MonaiAeTitle; + } + catch (ProblemException ex) + { + if (ex.ProblemDetails.Status == (int)HttpStatusCode.Conflict && + ex.ProblemDetails.Detail.Contains("already exists")) + { + await _informaticsGatewayClient.MonaiScpAeTitle.GetAeTitle(MonaiAeTitle, CancellationToken.None); + } + else + { + throw; + } + } + + var timeoutPolicy = Policy.TimeoutAsync(140, TimeoutStrategy.Pessimistic); + await timeoutPolicy + .ExecuteAsync( + async () => { await SendRequest(exportCount); } + ); + + // Clear workflow request messages + _receivedWorkflowRequestMessages.ClearMessages(); + _receivedArtifactRecievedMessages.ClearMessages(); + + _dataProvider.DimseRsponse.Should().Be(DicomStatus.Success); + + // Wait for workflow request events + (await _receivedArtifactRecievedMessages.WaitforAsync(1, MessageWaitTimeSpan)).Should().BeTrue(); + _assertions.ShouldHaveCorrectNumberOfWorkflowRequestMessages(_dataProvider, DataService.DIMSE, _receivedArtifactRecievedMessages.Messages, 1); + } + + [Then(@"ensure the original externalApp study and the received study are the same")] + public async Task ThenEnsureTheOriginalExternalAppStudyAndTheReceivedStudyAreTheSame() + { + var workflowRequestEvent = _receivedArtifactRecievedMessages.Messages[0].ConvertTo(); + _exportRequestEvent.CorrelationId.Should().Be(_receivedArtifactRecievedMessages.Messages[0].CorrelationId); + _exportRequestEvent.CorrelationId.Should().Be(workflowRequestEvent.CorrelationId); + _exportRequestEvent.WorkflowInstanceId.Should().Be(workflowRequestEvent.WorkflowInstanceId); + _exportRequestEvent.ExportTaskId.Should().Be(workflowRequestEvent.TaskId); + await _assertions.ShouldRestoreAllDicomMetaata(_receivedArtifactRecievedMessages.Messages, _originalDicomFiles, DefaultDicomTags.ToArray()).ConfigureAwait(false); + } + + private async Task SendRequest(int exportCount = 1) + { + // Wait for export completed event + (await _receivedExportCompletedMessages.WaitforAsync(exportCount, DicomScpWaitTimeSpan)).Should().BeTrue(); + + foreach (var key in _dataProvider.DicomSpecs.FileHashes.Keys) + { + (await Extensions.WaitUntil(() => _dicomServer.Instances.ContainsKey(key), DicomScpWaitTimeSpan)).Should().BeTrue("{0} should be received", key); + } + + // Send data received back to MIG + var storeScu = _objectContainer.Resolve("StoreSCU"); + + var host = _configuration.InformaticsGatewayOptions.Host; + var port = _informaticsGatewayConfiguration.Dicom.Scp.ExternalAppPort; + + _dataProvider.Workflows = null; + _dataProvider.DicomSpecs.Files.Clear(); + _dataProvider.DicomSpecs.Files = new Dictionary(_dicomServer.DicomFiles); + _dataProvider.DicomSpecs.Files.Should().NotBeNull(); + + await storeScu.SendAsync( + _dataProvider, + SourceAeTitle, + host, + port, + MonaiAeTitle); + } + } +} diff --git a/tests/Integration.Test/StepDefinitions/Hl7StepDefinitions.cs b/tests/Integration.Test/StepDefinitions/Hl7StepDefinitions.cs new file mode 100755 index 000000000..be93a8bce --- /dev/null +++ b/tests/Integration.Test/StepDefinitions/Hl7StepDefinitions.cs @@ -0,0 +1,237 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 BoDi; +using FellowOakDicom; +using Monai.Deploy.InformaticsGateway.Api.Models; +using Monai.Deploy.InformaticsGateway.Client; +using Monai.Deploy.InformaticsGateway.Client.Common; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Integration.Test.Common; +using Monai.Deploy.InformaticsGateway.Integration.Test.Drivers; +using Monai.Deploy.Messaging.Events; +using Monai.Deploy.Messaging.Messages; +using Monai.Deploy.Messaging.RabbitMQ; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Monai.Deploy.InformaticsGateway.Integration.Test.StepDefinitions +{ + [Binding] + [CollectionDefinition("SpecFlowNonParallelizableFeatures", DisableParallelization = true)] + + internal class Hl7StepDEfinitions + { + private static readonly TimeSpan MessageWaitTimeSpan = TimeSpan.FromMinutes(3); + private static readonly DicomTag[] DicomTags = new[] { DicomTag.AccessionNumber, DicomTag.StudyDescription, DicomTag.SeriesDescription, DicomTag.PatientAddress, DicomTag.PatientAge, DicomTag.PatientName }; + private static readonly List DefaultDicomTags = new() { DicomTag.PatientID, DicomTag.StudyInstanceUID, DicomTag.SeriesInstanceUID, DicomTag.SOPInstanceUID }; + + private readonly ObjectContainer _objectContainer; + private readonly InformaticsGatewayClient _informaticsGatewayClient; + private readonly IDataClient _dataSinkMinio; + private readonly DicomScp _dicomServer; + private readonly Configurations _configuration; + private string _dicomDestination; + private readonly DataProvider _dataProvider; + private readonly RabbitMqConsumer _receivedExportHL7CompletedMessages; + private readonly RabbitMQMessagePublisherService _messagePublisher; + private readonly InformaticsGatewayConfiguration _informaticsGatewayConfiguration; + private Dictionary _originalHL7Files; + private ExportRequestEvent _exportRequestEvent; + private readonly Assertions _assertions; + private readonly string _correlationId = Guid.NewGuid().ToString(); + private readonly string _exportTaskId = Guid.NewGuid().ToString(); + private readonly string _workflowInstanceId = Guid.NewGuid().ToString(); + internal static readonly TimeSpan WaitTimeSpan = TimeSpan.FromSeconds(30); + private readonly string _hl7SendAddress = "127.0.0.1"; + private readonly int _hl7Port = 2574; + private JsonMessage _messageToSend; + + private readonly List _hl7Messages = new List(); + private TcpListener _tcpListener; + + public Hl7StepDEfinitions(ObjectContainer objectContainer, Configurations configuration) + { + _objectContainer = objectContainer ?? throw new ArgumentNullException(nameof(objectContainer)); + _informaticsGatewayClient = objectContainer.Resolve("InformaticsGatewayClient"); + _dataSinkMinio = objectContainer.Resolve("MinioClient"); + _dicomServer = objectContainer.Resolve("DicomScp"); + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _dataProvider = objectContainer.Resolve("DataProvider"); + _receivedExportHL7CompletedMessages = objectContainer.Resolve("ExportHL7CompleteSubscriber"); + _messagePublisher = objectContainer.Resolve("MessagingPublisher"); + _informaticsGatewayConfiguration = objectContainer.Resolve("InformaticsGatewayConfiguration"); + _assertions = objectContainer.Resolve("Assertions"); + + DefaultDicomTags.AddRange(DicomTags); + _dicomServer.ClearFilesAndUseHashes = false; //we need to store actual files to send the data back to MIG + } + + [Given(@"a HL7 message that is exported to the test host")] + public async Task GivenAHLMessageThatIsExportedToTheTestHost() + { + HL7DestinationEntity destination; + try + { + destination = await _informaticsGatewayClient.HL7Destinations.Create(new HL7DestinationEntity + { + Name = _dicomServer.FeatureScpAeTitle, + AeTitle = _dicomServer.FeatureScpAeTitle, + HostIp = _hl7SendAddress, + Port = _hl7Port + }, CancellationToken.None); + } + catch (ProblemException ex) + { + if (ex.ProblemDetails.Status == (int)HttpStatusCode.Conflict && ex.ProblemDetails.Detail.Contains("already exists")) + { + destination = await _informaticsGatewayClient.HL7Destinations.GetAeTitle(_dicomServer.FeatureScpAeTitle, CancellationToken.None); + } + else + { + throw; + } + } + _dicomDestination = destination.Name; + + // Generate a study with multiple series + //_dataProvider.GenerateDicomData("MG", 1, 1); + await _dataProvider.GenerateHl7Messages("2.3"); + + _originalHL7Files = new Dictionary(_dataProvider.HL7Specs.Files); + + var path = "hl7filepath"; + await _dataSinkMinio.SaveHl7Async(_dataProvider, path); + + // Emit a export request event + _exportRequestEvent = new ExportRequestEvent + { + CorrelationId = _correlationId, + Destinations = new string[] { destination.Name }, + ExportTaskId = _exportTaskId, + Files = _dataProvider.HL7Specs.Files.Keys.Select(f => $"{path}/{f.Replace(".txt", ".hl7")}"), + MessageId = Guid.NewGuid().ToString(), + WorkflowInstanceId = _workflowInstanceId, + PayloadId = "ThisIs/My/Output/Folder", + }; + + _messageToSend = new JsonMessage( + _exportRequestEvent, + MessageBrokerConfiguration.InformaticsGatewayApplicationId, + _exportRequestEvent.CorrelationId, + string.Empty); + + } + + [When(@"the HL7 Export message is received with (.*) messages acked (.*)")] + public async Task WhenTheHL7ExportMessageIsReceivedWithMessagesAcked(int messageCount, bool acked) + { + var cancellationToken = new CancellationToken(); + + _tcpListener = new System.Net.Sockets.TcpListener(IPAddress.Parse(_hl7SendAddress), _hl7Port); + _tcpListener.Start(); + + await _messagePublisher.Publish("md.export.hl7", _messageToSend.ToMessage()); + + List recievedMessages = new List(); + + for (int i = 0; i < messageCount; i++) + { + using var _client = await _tcpListener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(false); + await GetMessageAsync(_client, acked, cancellationToken); + if (_hl7Messages.Count == messageCount) + { break; } + } + + _tcpListener.Stop(); + } + + private async Task GetMessageAsync(TcpClient _client, bool acked, CancellationToken cancellationToken) + { + var messages = new List(); + using var _clientStream = _client.GetStream(); + _clientStream.ReadTimeout = 5000; + _clientStream.WriteTimeout = 5000; + var buffer = new byte[10240]; + + var s_cts = new CancellationTokenSource(); + s_cts.CancelAfter(60000); + + var bytesRead = _clientStream.Read(buffer, 0, buffer.Length); + + + if (bytesRead == 0 || s_cts.IsCancellationRequested) + { + return; + } + + var data = Encoding.UTF8.GetString(buffer.ToArray()); + + var _rawHl7Messages = HL7.Dotnetcore.MessageHelper.ExtractMessages(data); + foreach (var message in _rawHl7Messages) + { + var hl7Message = new HL7.Dotnetcore.Message(message); + hl7Message.ParseMessage(); + _hl7Messages.Add(hl7Message); + if (acked) + { await SendAcknowledgment(_clientStream, hl7Message, cancellationToken); } + } + return; + } + + [Then(@"ensure that exportcomplete messages are sent with (.*)")] + public async Task ThenEnsureThatExportcompleteMessagesAreSentWithSuscess(string valid) + { + var success = await _receivedExportHL7CompletedMessages.WaitforAsync(1, TimeSpan.FromSeconds(600)); + Assert.Equal(1, _receivedExportHL7CompletedMessages.Messages.Count); + var message = _receivedExportHL7CompletedMessages.Messages.First(); + var exportEvent = message.ConvertTo(); + var status = exportEvent.Status; + if (valid == "success") + { + Assert.Equal(ExportStatus.Success, status); + } + else if (valid == "failure") + { + Assert.Equal(ExportStatus.Failure, status); + } + + foreach (var hl7message in _hl7Messages) + { + Assertions.ShouldBeInMessageDictionary(_originalHL7Files, hl7message); + } + } + + private async Task SendAcknowledgment(NetworkStream networkStream, HL7.Dotnetcore.Message message, CancellationToken cancellationToken) + { + if (message == null) { return; } + var ackMessage = message.GetACK(); + var ackData = new ReadOnlyMemory(ackMessage.GetMLLP()); + { + try + { + await networkStream.WriteAsync(ackData, cancellationToken).ConfigureAwait(false); + await networkStream.FlushAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception) + { + throw; + } + } + } + } +} diff --git a/tests/Integration.Test/StepDefinitions/RemoteAppExecutionPlugInsStepDefinitions.cs b/tests/Integration.Test/StepDefinitions/RemoteAppExecutionPlugInsStepDefinitions.cs index 02984c323..55e7137cf 100755 --- a/tests/Integration.Test/StepDefinitions/RemoteAppExecutionPlugInsStepDefinitions.cs +++ b/tests/Integration.Test/StepDefinitions/RemoteAppExecutionPlugInsStepDefinitions.cs @@ -19,12 +19,12 @@ using FellowOakDicom; using FellowOakDicom.Network; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Models; using Monai.Deploy.InformaticsGateway.Client; using Monai.Deploy.InformaticsGateway.Client.Common; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Integration.Test.Common; using Monai.Deploy.InformaticsGateway.Integration.Test.Drivers; -//using Monai.Deploy.InformaticsGateway.PlugIns.Pseudonymisation; using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution; using Monai.Deploy.Messaging.Events; using Monai.Deploy.Messaging.Messages; @@ -211,7 +211,7 @@ public async Task AStudyThatIsExportedToTheTestHostBadPlugin() } [When(@"the study is received and sent back to Informatics Gateway with (.*) messages")] - public async Task TheStudyIsReceivedAndSentBackToInformaticsGateway(int exportCount = 1) + public async Task WhenTheStudyIsReceivedAndSentBackToInformaticsGatewayWith(int exportCount) { // setup DICOM Source diff --git a/tests/Integration.Test/appsettings.json b/tests/Integration.Test/appsettings.json index 4268c4f29..8e3d304b6 100755 --- a/tests/Integration.Test/appsettings.json +++ b/tests/Integration.Test/appsettings.json @@ -5,6 +5,25 @@ "plugins": { "remoteApp": { "ReplaceTags": "AccessionNumber, StudyDescription, SeriesDescription, PatientAddress, PatientAge, PatientName" + }, + "Pseudonymise": { + "ConnectionString": "mongodb://root:rootpassword@localhost:27017", + "DatabaseName": "InformaticsGateway", + "EncriptionClientTimeoutSeconds": "900", + "ExpiresAfterDays": "1", + "ExternalAppTaskInboundKeepTags": "0020 000E, 0020 0010, 0020 00013, 0008 0018, 0020 000E", + "ImportantTags": "0010 0020, 0008 0018, 0008 0016, 0020 000D, 0020 000E, 0020 0010,0008 1155, 0008 0014, 0008 0050, 0008 0080, 0008 0081, 0008 0090, 0008 0092, 0008 0094, 0008 1010, 0008 1030, 0008 103E, 0008 1040, 0008 1048,0008 1050, 0008 1060, 0008 1070, 0008 1080, 0008 2111, 0010 0010, 0010 0030, 0010 0030, 0010 0032, 0010 0040, 0010 1000, 0010 1001, 0010 1001, 0010 1010, 0010 1020, 0010 1030, 0010 1090, 0010 2160, 0010 2180, 0010 21B0, 0010 4000, 0018 1000, 0018 1030, 0020 0052, 0020 0200, 0020 4000, 0040 0275, 0040 A124, 0040 A730, 0088 0140, 3006 0024, 3006 00C2", + "SecurityProfile": "\t\t\t\t0010,0020;K;;;;;;;;;;\r\n\t\t\t\t0008,0018;C;;;;;;;;;;\r\n\t\t\t\t0008,0016;K;;;;;;;;;;\r\n\t\t\t\t0020,000D;K;;;;;;;;;;\r\n\t\t\t\t0020,000E;C;;;;;;;;;;\r\n\t\t\t\t0020,0010;C;;;;;;;;;;\r\n\t\t\t\t0008,1155;C;;;;;;;;;;\r\n\t\t\t\t0008,0014;X;;;;;;;;;;\r\n\t\t\t\t0008,0050;K;;;;;;;;;;\r\n\t\t\t\t0008,0080;X;;;;;;;;;;\r\n\t\t\t\t0008,0081;X;;;;;;;;;;\r\n\t\t\t\t0008,0090;X;;;;;;;;;;\r\n\t\t\t\t0008,0092;X;;;;;;;;;;\r\n\t\t\t\t0008,0094;X;;;;;;;;;;\r\n\t\t\t\t0008,1010;X;;;;;;;;;;\r\n\t\t\t\t0008,1030;X;;;;;;;;;;\r\n\t\t\t\t0008,103E;X;;;;;;;;;;\r\n\t\t\t\t0008,1040;X;;;;;;;;;;\r\n\t\t\t\t0008,1048;X;;;;;;;;;;\r\n\t\t\t\t0008,1050;X;;;;;;;;;;\r\n\t\t\t\t0008,1060;X;;;;;;;;;;\r\n\t\t\t\t0008,1070;X;;;;;;;;;;\r\n\t\t\t\t0008,1080;X;;;;;;;;;;\r\n\t\t\t\t0008,2111;X;;;;;;;;;;\r\n\t\t\t\t0010,0010;X;;;;;;;;;;\r\n\t\t\t\t0010,0030;X;;;;;;;;;;\r\n\t\t\t\t0010,0030;X;;;;;;;;;;\r\n\t\t\t\t0010,0032;X;;;;;;;;;;\r\n\t\t\t\t0010,0040;X;;;;;;;;;;\r\n\t\t\t\t0010,1000;X;;;;;;;;;;\r\n\t\t\t\t0010,1001;X;;;;;;;;;;\r\n\t\t\t\t0010,1001;X;;;;;;;;;;\r\n\t\t\t\t0010,1010;X;;;;;;;;;;\r\n\t\t\t\t0010,1020;X;;;;;;;;;;\r\n\t\t\t\t0010,1030;X;;;;;;;;;;\r\n\t\t\t\t0010,1090;X;;;;;;;;;;\r\n\t\t\t\t0010,2160;X;;;;;;;;;;\r\n\t\t\t\t0010,2180;X;;;;;;;;;;\r\n\t\t\t\t0010,21B0;X;;;;;;;;;;\r\n\t\t\t\t0010,4000;X;;;;;;;;;;\r\n\t\t\t\t\r\n\t\t\t\t0018,1000;X;;;;;;;;;;\r\n\t\t\t\t0018,1030;X;;;;;;;;;;\r\n\t\t\t\t0020,0052;X;;;;;;;;;;\r\n\t\t\t\t0020,0200;X;;;;;;;;;;\r\n\t\t\t\t0020,4000;X;;;;;;;;;;\r\n\t\t\t\t0040,0275;X;;;;;;;;;;\r\n\t\t\t\t0040,A124;X;;;;;;;;;;\r\n\t\t\t\t0040,A730;X;;;;;;;;;;\r\n\t\t\t\t0088,0140;X;;;;;;;;;;\r\n\t\t\t\t3006,0024;X;;;;;;;;;;\r\n\t\t\t\t3006,00C2;X;;;;;;;;;;\r\n\t\t\t\t", + "KMS": { + "KeyVaultNamespace": "InformaticsGateway.KeyVault", + "AWS": { + "accessKeyId": "", + "arnKey": "", + "roleArnToAssume": "", + "region": "eu-west-2", + "secretAccessKey": "" + } + } } }, "ConnectionStrings": { @@ -25,6 +44,7 @@ "dicom": { "scp": { "port": 1104, + "externalAppPort": 1106, "logDimseDatasets": false, "rejectUnknownSources": true }, @@ -80,7 +100,7 @@ "hl7": { "port": 2575, "maximumNumberOfConnections": 10, - "clientTimeout": 60000, + "clientTimeout": 200, "sendAck": true } }, diff --git a/tests/Integration.Test/packages.lock.json b/tests/Integration.Test/packages.lock.json index ea3e1fea5..9a208b6ce 100755 --- a/tests/Integration.Test/packages.lock.json +++ b/tests/Integration.Test/packages.lock.json @@ -44,12 +44,12 @@ }, "Microsoft.EntityFrameworkCore": { "type": "Direct", - "requested": "[6.0.22, )", - "resolved": "6.0.22", - "contentHash": "vNe+y8ZsEf1CsfmfYttfKAz/IgCCtphgguvao0HWNJNdjZf9cabD288nZJ17b/WaQMWXhLwYAsofk8vNVkfTOA==", + "requested": "[6.0.25, )", + "resolved": "6.0.25", + "contentHash": "txcqw2xrmvMoTIgzAdUk8JHLELofGgTK3i6glswVZs4SC8BOU1M/iSAtwMIVtAtfzxuBIUAbHPx+Ly6lfkYe7g==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "6.0.22", - "Microsoft.EntityFrameworkCore.Analyzers": "6.0.22", + "Microsoft.EntityFrameworkCore.Abstractions": "6.0.25", + "Microsoft.EntityFrameworkCore.Analyzers": "6.0.25", "Microsoft.Extensions.Caching.Memory": "6.0.1", "Microsoft.Extensions.DependencyInjection": "6.0.1", "Microsoft.Extensions.Logging": "6.0.0", @@ -59,11 +59,11 @@ }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "Direct", - "requested": "[6.0.22, )", - "resolved": "6.0.22", - "contentHash": "EDKnYZtxq7P131xxLsEokda86WnFRiVAveLVAYR8kzyWl/UwTpf/RS2m2FrbH/U8vX3A+IQNpabtxcjtCUrY0g==", + "requested": "[6.0.25, )", + "resolved": "6.0.25", + "contentHash": "vaQNuXgUN0nIzFXQiPSb9iAaJqLvZA164Sx9mjF5rFQS5cwQ/AiymF0e4J0QH3P07Mf3zEVZE5u2fTO0NacuMQ==", "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.22", + "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.25", "SQLitePCLRaw.bundle_e_sqlite3": "2.1.2" } }, @@ -132,11 +132,11 @@ }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Direct", - "requested": "[1.0.4, )", - "resolved": "1.0.4", - "contentHash": "2llZ4XbE91Km2Q+JEKSSeTyhZLWRq3lN5xQ6+Klqow3V8SXBAlOQQ+b5//BEm6x0QdoycFberMOVAsZYYM0j7g==", + "requested": "[1.0.5, )", + "resolved": "1.0.5", + "contentHash": "L+BWU5Xq1ARjFRcpnefDJGuG52Zw4Iz3qql1tn8lYfqoC4B37fAUVz6k7Ar7v1OUwPo/JR8q4OP2IIMpqpKRRA==", "dependencies": { - "Monai.Deploy.Messaging": "1.0.4", + "Monai.Deploy.Messaging": "1.0.5", "Polly": "7.2.4", "RabbitMQ.Client": "6.5.0" } @@ -226,6 +226,16 @@ "resolved": "2.5.0", "contentHash": "+Gp9vuC2431yPyKB15YrOTxCuEAErBQUTIs6CquumX1F073UaPHGW0VE/XVJLMh9W4sXdz3TBkcHdFWZrRn2Hw==" }, + "AnswerDicomTools": { + "type": "Transitive", + "resolved": "0.1.1-rc0089", + "contentHash": "DgDBjo708kHmr0lUdUrYaRLjmaICrJMBh6w/Vd0E/r2SJ0DDiQuxMABJzIwXwrVFSzrrwpqPqWBQMTCY++9uPQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "6.0.1", + "MongoDB.Driver": "2.21.0", + "fo-dicom": "5.1.1" + } + }, "Ardalis.GuardClauses": { "type": "Transitive", "resolved": "4.1.1", @@ -345,38 +355,38 @@ }, "Microsoft.Data.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "gtIGHbGnRq/h4mFSJYr9BdMObvJV/a67nBubs50VjPDusQARtWJzeVTirDWsbL1qTvGzbbZCD7VE7+s2ixZfow==", + "resolved": "6.0.25", + "contentHash": "rbXNoMg/ylGyJxLcyetojuXFzvDG85M31DfFbqL8veN4P8oG6wmnPwWNn3/bDIEDVvdw15R092dxpobQeQcjGg==", "dependencies": { "SQLitePCLRaw.core": "2.1.2" } }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "3ycEYrtWoa4kv5mUECU2LNBbWiYh345b1uQLvg4pHCEICXoJZ8Sfu/2yGloKiMNgMdDc02gFYCRHxsqQNZpnWA==" + "resolved": "6.0.25", + "contentHash": "DalO25C96LsIfAPlyizyun9y1XrIquRugPEGXC8+z7dFo+GyU0LRd0R11JDd3rJWjR18NOFYwqNenjyDpNRO3A==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "82SZfdrLe7bdDB8/3INV0UULvlUzsdHkrEYylDCrzFXRWHXG9eO5jJQjRHU8j9XkGIN+MSPgIlczBnqeDvB36A==" + "resolved": "6.0.25", + "contentHash": "i6UpdWqWxSBbIFOkaMoubM40yIjTZO+0rIUkY5JRltSeFI4PzncBBQcNVNXXjAmiLXF/xY0xTS+ykClbkV46Yg==" }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "W7yfdEbEuS1OPPxU0EOA6haqI4uvzs7OwHKh81DiJFn3NFNP2ztSovkOzBDhTwHX0j+OySsAj3BEJhuzTVYIVw==", + "resolved": "6.0.25", + "contentHash": "ci2lR++x7R7LR71+HoeRnB9Z5VeOQ1ILLbFRhsjjWZyLrAMkdq7TK9Ll47jo1TXDWF8Ddeap1JgcptgPKkWSRA==", "dependencies": { - "Microsoft.EntityFrameworkCore": "6.0.22", + "Microsoft.EntityFrameworkCore": "6.0.25", "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" } }, "Microsoft.EntityFrameworkCore.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "xSU77ORQgwlD+s5Cmlk9DzoSCu5oxlHLuQl+v5zAZ0Uv5yH17hp02TBfz3x9nBA+CrIsqaLjGEuyZmLDf/5ATw==", + "resolved": "6.0.25", + "contentHash": "IU4E8I9FS2sUVxJJ0w/4jogLQ8C0zvu/SO6b1tRmiiCtTrHhjUB0tqhxjrFnDXZ/mpCJOElw50+qhbcElm0CYw==", "dependencies": { - "Microsoft.Data.Sqlite.Core": "6.0.22", - "Microsoft.EntityFrameworkCore.Relational": "6.0.22", + "Microsoft.Data.Sqlite.Core": "6.0.25", + "Microsoft.EntityFrameworkCore.Relational": "6.0.25", "Microsoft.Extensions.DependencyModel": "6.0.0" } }, @@ -413,6 +423,15 @@ "Microsoft.Extensions.Primitives": "6.0.0" } }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "3nL1qCkZ1Oxx14ZTzgo4MmlO7tso7F+TtMZAY2jUAtTLyAcDp+EDjk3RqafoKiNaePyPvvlleEcBxh3b2Hzl1g==", + "dependencies": { + "Microsoft.Extensions.Configuration": "6.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" + } + }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Transitive", "resolved": "6.0.0", @@ -425,6 +444,17 @@ "Microsoft.Extensions.Primitives": "6.0.0" } }, + "Microsoft.Extensions.Configuration.UserSecrets": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "Fy8yr4V6obi7ZxvKYI1i85jqtwMq8tqyxQVZpRSkgeA8enqy/KvBIMdcuNdznlxQMZa72mvbHqb7vbg4Pyx95w==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.Configuration.Json": "6.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", + "Microsoft.Extensions.FileProviders.Physical": "6.0.0" + } + }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "6.0.1", @@ -453,10 +483,10 @@ }, "Microsoft.Extensions.Diagnostics.HealthChecks": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "HB1Zp1NY9m+HwYKLZBgUfNIt0xXzm4APARDuAIPODl8pT4g10oOiEDN8asOzx/sfL9xM+Sse5Zne9L+6qYi/iA==", + "resolved": "6.0.25", + "contentHash": "9vz47iGkzqhh0bGqomOTxaJNEEajeNcbSTSWwhh9Soo9lWm0UdPbw04CxXCQJPhc0aw9OaMnOxx7sCcde8/adA==", "dependencies": { - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.22", + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.25", "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", "Microsoft.Extensions.Logging.Abstractions": "6.0.4", "Microsoft.Extensions.Options": "6.0.0" @@ -464,17 +494,17 @@ }, "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "yvz+0r3qAt6gNEKlGSBO1BXMhtD3Tt8yzU59dHASolpwlSHvgqy0tEP6KXn3MPoKlPr0CiAHUdzOwYSoljzRSg==" + "resolved": "6.0.25", + "contentHash": "9sd1K/rp/vlxrBWNa0i8fgHCBPg94cocGMsJr7z9e2zQGQxMHNGpspdcy/FRGPAh2CINQet/RrM6Ef196xI20w==" }, "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "PNj+/e/GCJh3ZNzxEGhkMpKJgmmbuGar6Uk/R3mPFZacTx6lBdLs4Ev7uf4XQWqTdJe56rK+2P3oF/9jIGbxgw==", + "resolved": "6.0.25", + "contentHash": "Cmhq0sgb53+dh9xHOlBEQUhi13vsZeQ4fcYC9JYO4med7pabj9x3100opCdUv+7UX+tUC1GPm/nco+1skJdLFA==", "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "6.0.22", - "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.22", - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.22" + "Microsoft.EntityFrameworkCore.Relational": "6.0.25", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.25", + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.25" } }, "Microsoft.Extensions.FileProviders.Abstractions": { @@ -500,6 +530,34 @@ "resolved": "6.0.0", "contentHash": "ip8jnL1aPiaPeKINCqaTEbvBFDmVx9dXQEBZ2HOBRXPD1eabGNqP/bKlsIcp7U2lGxiXd5xIhoFcmY8nM4Hdiw==" }, + "Microsoft.Extensions.Hosting": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "hbmizc9KPWOacLU8Z8YMaBG6KWdZFppczYV/KwnPGU/8xebWxQxdDeJmLOgg968prb7g2oQgnp6JVLX6lgby8g==", + "dependencies": { + "Microsoft.Extensions.Configuration": "6.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.Configuration.Binder": "6.0.0", + "Microsoft.Extensions.Configuration.CommandLine": "6.0.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "6.0.1", + "Microsoft.Extensions.Configuration.FileExtensions": "6.0.0", + "Microsoft.Extensions.Configuration.Json": "6.0.0", + "Microsoft.Extensions.Configuration.UserSecrets": "6.0.1", + "Microsoft.Extensions.DependencyInjection": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", + "Microsoft.Extensions.FileProviders.Physical": "6.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Configuration": "6.0.0", + "Microsoft.Extensions.Logging.Console": "6.0.0", + "Microsoft.Extensions.Logging.Debug": "6.0.0", + "Microsoft.Extensions.Logging.EventLog": "6.0.0", + "Microsoft.Extensions.Logging.EventSource": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0" + } + }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", "resolved": "6.0.0", @@ -542,6 +600,55 @@ "Microsoft.Extensions.Options.ConfigurationExtensions": "6.0.0" } }, + "Microsoft.Extensions.Logging.Console": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "gsqKzOEdsvq28QiXFxagmn1oRB9GeI5GgYCkoybZtQA0IUb7QPwf1WmN3AwJeNIsadTvIFQCiVK0OVIgKfOBGg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Configuration": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "System.Text.Json": "6.0.0" + } + }, + "Microsoft.Extensions.Logging.Debug": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "M9g/JixseSZATJE9tcMn9uzoD4+DbSglivFqVx8YkRJ7VVPmnvCEbOZ0AAaxsL1EKyI4cz07DXOOJExxNsUOHw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0" + } + }, + "Microsoft.Extensions.Logging.EventLog": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "rlo0RxlMd0WtLG3CHI0qOTp6fFn7MvQjlrCjucA31RqmiMFCZkF8CHNbe8O7tbBIyyoLGWB1he9CbaA5iyHthg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "System.Diagnostics.EventLog": "6.0.0" + } + }, + "Microsoft.Extensions.Logging.EventSource": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "BeDyyqt7nkm/nr+Gdk+L8n1tUT/u33VkbXAOesgYSNsxDM9hJ1NOBGoZfj9rCbeD2+9myElI6JOVVFmnzgeWQA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Json": "6.0.0" + } + }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "6.0.0", @@ -666,8 +773,8 @@ }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "K6RrbDh7upokvt+sKuKEhQ+B1Xj46DF4sHxqwE6ymZazwmRULzsD0u/1IeDDJCGuRs3iG64QWwCt32j30PSZLg==", + "resolved": "1.0.5", + "contentHash": "J8Lskfy8PSVQLDE2uLqh53uaPpqpRJuSGVHpR2jrw+GYnTTDv21j/2gxwG8Hq2NgNOkWLNVi+fFnyWd6WFiUTA==", "dependencies": { "Ardalis.GuardClauses": "4.1.1", "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", @@ -1941,6 +2048,25 @@ "resolved": "0.6.2", "contentHash": "jPao/LdUNLUz8rn3H1D8W7wQbZsRZM0iayvWI4xGejJg3XJHT56gcmYdgmCGPdJF1UEBqUjucCRrFB+4HbJsbw==" }, + "monai-deploy-informatics-gateway-pseudonymisation": { + "type": "Project", + "dependencies": { + "AnswerDicomTools": "[0.1.1-rc0089, )", + "Ardalis.GuardClauses": "[4.1.1, )", + "HL7-dotnetcore": "[2.36.0, )", + "Microsoft.EntityFrameworkCore": "[6.0.25, )", + "Microsoft.EntityFrameworkCore.Relational": "[6.0.25, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.25, )", + "Microsoft.Extensions.Configuration": "[6.0.1, )", + "Microsoft.Extensions.Configuration.FileExtensions": "[6.0.0, )", + "Microsoft.Extensions.Configuration.Json": "[6.0.0, )", + "Microsoft.Extensions.Hosting": "[6.0.1, )", + "MongoDB.Driver": "[2.21.0, )", + "NLog": "[5.2.4, )", + "Polly": "[7.2.4, )", + "fo-dicom": "[5.1.1, )" + } + }, "monai.deploy.informaticsgateway": { "type": "Project", "dependencies": { @@ -1954,7 +2080,7 @@ "Monai.Deploy.InformaticsGateway.Database.EntityFramework": "[1.0.0, )", "Monai.Deploy.InformaticsGateway.DicomWeb.Client": "[1.0.0, )", "Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution": "[1.0.0, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Security": "[0.1.3, )", "Monai.Deploy.Storage.MinIO": "[0.2.18, )", "NLog.Web.AspNetCore": "[5.3.4, )", @@ -1964,11 +2090,12 @@ "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { + "HL7-dotnetcore": "[2.36.0, )", "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.22, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.25, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[1.0.4, )", - "Monai.Deploy.Messaging.RabbitMQ": "[1.0.4, )", + "Monai.Deploy.Messaging": "[1.0.5, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.5, )", "Monai.Deploy.Storage": "[0.2.18, )", "fo-dicom": "[5.1.1, )" } @@ -2004,7 +2131,7 @@ "type": "Project", "dependencies": { "AspNetCore.HealthChecks.MongoDb": "[6.0.2, )", - "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "[6.0.22, )", + "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "[6.0.25, )", "Microsoft.Extensions.Options.ConfigurationExtensions": "[6.0.0, )", "Monai.Deploy.InformaticsGateway.Api": "[0.4.1, )", "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )", @@ -2024,8 +2151,8 @@ "monai.deploy.informaticsgateway.database.entityframework": { "type": "Project", "dependencies": { - "Microsoft.EntityFrameworkCore": "[6.0.22, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.22, )", + "Microsoft.EntityFrameworkCore": "[6.0.25, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.25, )", "Microsoft.Extensions.Configuration.FileExtensions": "[6.0.0, )", "Microsoft.Extensions.Configuration.Json": "[6.0.0, )", "Monai.Deploy.InformaticsGateway.Api": "[0.4.1, )", @@ -2054,9 +2181,9 @@ "monai.deploy.informaticsgateway.plugins.remoteappexecution": { "type": "Project", "dependencies": { - "Microsoft.EntityFrameworkCore": "[6.0.22, )", - "Microsoft.EntityFrameworkCore.Relational": "[6.0.22, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.22, )", + "Microsoft.EntityFrameworkCore": "[6.0.25, )", + "Microsoft.EntityFrameworkCore.Relational": "[6.0.25, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.25, )", "Microsoft.Extensions.Configuration": "[6.0.1, )", "Microsoft.Extensions.Configuration.FileExtensions": "[6.0.0, )", "Microsoft.Extensions.Configuration.Json": "[6.0.0, )",