diff --git a/src/Api/Storage/FileStorageMetadata.cs b/src/Api/Storage/FileStorageMetadata.cs old mode 100644 new mode 100755 diff --git a/src/Api/Storage/Payload.cs b/src/Api/Storage/Payload.cs old mode 100644 new mode 100755 index c9fbbc50b..77e3fae5a --- a/src/Api/Storage/Payload.cs +++ b/src/Api/Storage/Payload.cs @@ -19,6 +19,7 @@ using System.Diagnostics; using System.Linq; using Ardalis.GuardClauses; +using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.Messaging.Events; namespace Monai.Deploy.InformaticsGateway.Api.Storage @@ -40,7 +41,12 @@ public enum PayloadState /// /// Payload is ready to be published to the message broker. /// - Notify + Notify, + + /// + /// Payload has been finished with. + /// + UploadComplete } public const int MAX_RETRY = 3; @@ -86,6 +92,8 @@ public TimeSpan Elapsed public int FilesFailedToUpload { get => Files.Count(p => p.IsUploadFailed); } + public PatientDetails? PatientDetails { get; set; } + public Payload(string key, string correlationId, string? workflowInstanceId, string? taskId, DataOrigin dataTrigger, uint timeout) { Guard.Against.NullOrWhiteSpace(key, nameof(key)); @@ -158,4 +166,4 @@ public void Dispose() GC.SuppressFinalize(this); } } -} \ No newline at end of file +} diff --git a/src/Common/PatientDetails.cs b/src/Common/PatientDetails.cs new file mode 100755 index 000000000..f576cc9ff --- /dev/null +++ b/src/Common/PatientDetails.cs @@ -0,0 +1,48 @@ +/* + * 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.Text.Json; +using System.Text.Json.Serialization; + +namespace Monai.Deploy.InformaticsGateway.Common +{ + public class PatientDetails + { + [JsonPropertyName("patient_id")] + public string? PatientId { get; set; } + + [JsonPropertyName("patient_name")] + public string? PatientName { get; set; } + + [JsonPropertyName("patient_sex")] + public string? PatientSex { get; set; } + + [JsonPropertyName("patient_dob")] + public DateTime? PatientDob { get; set; } + + [JsonPropertyName("patient_age")] + public string? PatientAge { get; set; } + + [JsonPropertyName("patient_hospital_id")] + public string? PatientHospitalId { get; set; } + + public override string ToString() + { + return JsonSerializer.Serialize(this); + } + } +} diff --git a/src/InformaticsGateway/Logging/Log.6000.DicomService.cs b/src/InformaticsGateway/Logging/Log.6000.DicomService.cs new file mode 100755 index 000000000..b11a71dc0 --- /dev/null +++ b/src/InformaticsGateway/Logging/Log.6000.DicomService.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace Monai.Deploy.InformaticsGateway.Logging +{ + public static partial class Log + { + [LoggerMessage(EventId = 6000, Level = LogLevel.Error, Message = "Failed to get DICOM tag {dicomTag} in bucket {bucketId}. Payload: {payloadId}")] + public static partial void FailedToGetDicomTagFromPayload(this ILogger logger, string payloadId, string dicomTag, string bucketId, Exception ex); + + [LoggerMessage(EventId = 6001, Level = LogLevel.Information, Message = "Attempted to retrieve Patient Name from DCM file, result: {name}")] + public static partial void GetPatientName(this ILogger logger, string name); + + [LoggerMessage(EventId = 6002, Level = LogLevel.Information, Message = "Unsupported Type '{vr}' {vrFull} with value: {value} result: '{result}'")] + public static partial void UnsupportedType(this ILogger logger, string vr, string vrFull, string value, string result); + + [LoggerMessage(EventId = 6003, Level = LogLevel.Information, Message = "Decoding supported type '{vr}' {vrFull} with value: {value} result: '{result}'")] + public static partial void SupportedType(this ILogger logger, string vr, string vrFull, string value, string result); + + [LoggerMessage(EventId = 6004, Level = LogLevel.Error, Message = "Failed trying to cast Dicom Value to string {value}")] + public static partial void UnableToCastDicomValueToString(this ILogger logger, string value, Exception ex); + + [LoggerMessage(EventId = 6005, Level = LogLevel.Debug, Message = "Dicom export marked as succeeded with {fileStatusCount} files marked as exported.")] + public static partial void DicomExportSucceeded(this ILogger logger, string fileStatusCount); + + [LoggerMessage(EventId = 6006, Level = LogLevel.Debug, Message = "Dicom export marked as failed with {fileStatusCount} files marked as exported.")] + public static partial void DicomExportFailed(this ILogger logger, string fileStatusCount); + } +} diff --git a/src/InformaticsGateway/Program.cs b/src/InformaticsGateway/Program.cs index 273a07cff..a340291e7 100755 --- a/src/InformaticsGateway/Program.cs +++ b/src/InformaticsGateway/Program.cs @@ -119,6 +119,7 @@ internal static IHostBuilder CreateHostBuilder(string[] args) => services.AddScoped(); services.AddScoped, InputDataPlugInEngineFactory>(); services.AddScoped, OutputDataPlugInEngineFactory>(); + services.AddScoped(); services.AddMonaiDeployStorageService(hostContext.Configuration.GetSection("InformaticsGateway:storage:serviceAssemblyName").Value, Monai.Deploy.Storage.HealthCheckOptions.ServiceHealthCheck); diff --git a/src/InformaticsGateway/Services/Common/DicomService.cs b/src/InformaticsGateway/Services/Common/DicomService.cs new file mode 100755 index 000000000..5142f362e --- /dev/null +++ b/src/InformaticsGateway/Services/Common/DicomService.cs @@ -0,0 +1,353 @@ +/* + * 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.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Ardalis.GuardClauses; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Logging; +using Monai.Deploy.Storage.API; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Monai.Deploy.InformaticsGateway.Services.Common +{ + public class DicomService : IDicomService + { + private readonly IStorageService _storageService; + private readonly ILogger _logger; + + public DicomService(IStorageService storageService, ILogger logger) + { + _storageService = storageService ?? throw new ArgumentNullException(nameof(storageService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + private static readonly Dictionary SupportedTypes = new() + { + { "CS", "Code String" }, + { "DA", "Date" }, + { "DS", "Decimal String" }, + { "IS", "Integer String" }, + { "LO", "Long String" }, + { "SH", "Short String" }, + { "UI", "Unique Identifier (UID)" }, + { "UL", "Unsigned Long" }, + { "US", "Unsigned Short" }, + }; + + private static readonly Dictionary UnsupportedTypes = new() + { + { "CS", "Code String" }, + { "DA", "Date" }, + { "DS", "Decimal String" }, + { "IS", "Integer String" }, + { "LO", "Long String" }, + { "SH", "Short String" }, + { "UI", "Unique Identifier (UID)" }, + { "UL", "Unsigned Long" }, + { "US", "Unsigned Short" }, + }; + + public async Task GetPayloadPatientDetailsAsync(string payloadId, string bucketName) + { + Guard.Against.NullOrWhiteSpace(bucketName, nameof(bucketName)); + Guard.Against.NullOrWhiteSpace(payloadId, nameof(payloadId)); + + var items = await _storageService.ListObjectsAsync(bucketName, $"{payloadId}/dcm", true); + + var patientDetails = new PatientDetails + { + PatientName = await GetFirstValueAsync(items, payloadId, bucketName, DicomTagConstants.PatientNameTag), + PatientId = await GetFirstValueAsync(items, payloadId, bucketName, DicomTagConstants.PatientIdTag), + PatientSex = await GetFirstValueAsync(items, payloadId, bucketName, DicomTagConstants.PatientSexTag), + PatientAge = await GetFirstValueAsync(items, payloadId, bucketName, DicomTagConstants.PatientAgeTag), + PatientHospitalId = await GetFirstValueAsync(items, payloadId, bucketName, DicomTagConstants.PatientHospitalIdTag) + }; + + var dob = await GetFirstValueAsync(items, payloadId, bucketName, DicomTagConstants.PatientDateOfBirthTag); + + if (DateTime.TryParseExact(dob, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateOfBirth)) + { + patientDetails.PatientDob = dateOfBirth; + } + + return patientDetails; + } + + public async Task GetFirstValueAsync(IList items, string payloadId, string bucketId, string keyId) + { + Guard.Against.NullOrWhiteSpace(bucketId, nameof(bucketId)); + Guard.Against.NullOrWhiteSpace(payloadId, nameof(payloadId)); + Guard.Against.NullOrWhiteSpace(keyId, nameof(keyId)); + + try + { + if (items is null || items.Any() is false) + { + return null; + } + + foreach (var filePath in items.Select(item => item.FilePath)) + { + if (filePath.EndsWith(".dcm.json") is false) + { + continue; + } + + var stream = await _storageService.GetObjectAsync(bucketId, filePath); + var jsonStr = Encoding.UTF8.GetString(((MemoryStream)stream).ToArray()); + + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + JsonConvert.PopulateObject(jsonStr, dict); + + var value = GetValue(dict, keyId); + + if (!string.IsNullOrWhiteSpace(value)) + { + return value; + } + } + } + catch (Exception e) + { + _logger.FailedToGetDicomTagFromPayload(payloadId, keyId, bucketId, e); + } + + return null; + } + + public async Task> GetDicomPathsForTaskAsync(string outputDirectory, string bucketName) + { + Guard.Against.NullOrWhiteSpace(outputDirectory, nameof(outputDirectory)); + Guard.Against.NullOrWhiteSpace(bucketName, nameof(bucketName)); + + var files = await _storageService.ListObjectsAsync(bucketName, outputDirectory, true); + + var dicomFiles = files?.Where(f => f.FilePath.EndsWith(".dcm")); + + return dicomFiles?.Select(d => d.FilePath)?.ToList() ?? new List(); + } + + public async Task GetAnyValueAsync(string keyId, string payloadId, string bucketId) + { + Guard.Against.NullOrWhiteSpace(keyId, nameof(keyId)); + Guard.Against.NullOrWhiteSpace(payloadId, nameof(payloadId)); + Guard.Against.NullOrWhiteSpace(bucketId, nameof(bucketId)); + + var path = $"{payloadId}/dcm"; + var listOfFiles = await _storageService.ListObjectsAsync(bucketId, path, true); + var listOfJsonFiles = listOfFiles.Where(file => file.Filename.EndsWith(".json")).ToList(); + var fileCount = listOfJsonFiles.Count; + + for (int i = 0; i < fileCount; i++) + { + var matchValue = await GetDcmJsonFileValueAtIndexAsync(i, path, bucketId, keyId, listOfJsonFiles); + + if (matchValue != null) + { + return matchValue; + } + } + + return string.Empty; + } + + public async Task GetAllValueAsync(string keyId, string payloadId, string bucketId) + { + Guard.Against.NullOrWhiteSpace(keyId, nameof(keyId)); + Guard.Against.NullOrWhiteSpace(payloadId, nameof(payloadId)); + Guard.Against.NullOrWhiteSpace(bucketId, nameof(bucketId)); + + var path = $"{payloadId}/dcm"; + var listOfFiles = await _storageService.ListObjectsAsync(bucketId, path, true); + var listOfJsonFiles = listOfFiles.Where(file => file.Filename.EndsWith(".json")).ToList(); + var matchValue = await GetDcmJsonFileValueAtIndexAsync(0, path, bucketId, keyId, listOfJsonFiles); + var fileCount = listOfJsonFiles.Count; + + for (int i = 0; i < fileCount; i++) + { + if (listOfJsonFiles[i].Filename.EndsWith(".dcm")) + { + var currentValue = await GetDcmJsonFileValueAtIndexAsync(i, path, bucketId, keyId, listOfJsonFiles); + if (currentValue != matchValue) + { + return string.Empty; + } + } + } + + return matchValue; + } + + /// + /// Gets file at position + /// + /// + /// + /// + /// + /// + public async Task GetDcmJsonFileValueAtIndexAsync(int index, + string path, + string bucketId, + string keyId, + List items) + { + Guard.Against.NullOrWhiteSpace(bucketId, nameof(bucketId)); + Guard.Against.NullOrWhiteSpace(path, nameof(path)); + Guard.Against.NullOrWhiteSpace(keyId, nameof(keyId)); + Guard.Against.Null(items, nameof(items)); + + if (index > items.Count) + { + return string.Empty; + } + + var stream = await _storageService.GetObjectAsync(bucketId, items[index].FilePath); + var jsonStr = Encoding.UTF8.GetString(((MemoryStream)stream).ToArray()); + + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + JsonConvert.PopulateObject(jsonStr, dict); + return GetValue(dict, keyId); + } + + public string GetValue(Dictionary dict, string keyId) + { + if (dict.Any() is false) + { + return string.Empty; + } + + var result = string.Empty; + + if (dict.TryGetValue(keyId, out var value)) + { + if (string.Equals(keyId, DicomTagConstants.PatientNameTag) || value.Vr.ToUpperInvariant() == "PN") + { + result = GetPatientName(value.Value); + _logger.GetPatientName(result); + return result; + } + var jsonString = DecodeComplexString(value); + if (SupportedTypes.TryGetValue(value.Vr.ToUpperInvariant(), out var vrFullString)) + { + result = TryGetValueAndLogSupported(vrFullString, value, jsonString); + } + else if (UnsupportedTypes.TryGetValue(value.Vr.ToUpperInvariant(), out vrFullString)) + { + result = TryGetValueAndLogSupported(vrFullString, value, jsonString); + } + else + { + result = TryGetValueAndLogUnSupported("Unknown Dicom Type", value, jsonString); + } + } + return result; + } + + private string TryGetValueAndLogSupported(string vrFullString, DicomValue value, string jsonString) + { + var result = TryGetValue(value); + _logger.SupportedType(value.Vr, vrFullString, jsonString, result); + return result; + } + + private string TryGetValueAndLogUnSupported(string vrFullString, DicomValue value, string jsonString) + { + var result = TryGetValue(value); + _logger.UnsupportedType(value.Vr, vrFullString, jsonString, result); + return result; + } + + private string TryGetValue(DicomValue value) + { + var result = string.Empty; + foreach (var val in value.Value) + { + try + { + if (double.TryParse(val.ToString(), out var dbl)) + { + result = ConcatResult(result, dbl); + } + else + { + result = ConcatResult(result, val); + } + } + catch (Exception ex) + { + _logger.UnableToCastDicomValueToString(DecodeComplexString(value), ex); + } + } + if (value.Value.Length > 1) + { + return $"[{result}]"; + } + return result; + } + + private static string ConcatResult(string result, dynamic str) + { + if (string.IsNullOrWhiteSpace(result)) + { + result = string.Concat(result, $"{str}"); + } + else + { + result = string.Concat(result, $", {str}"); + } + + return result; + } + + private static string DecodeComplexString(DicomValue dicomValue) + { + return JsonConvert.SerializeObject(dicomValue.Value); + } + + private static string GetPatientName(object[] values) + { + + var resultStr = new List(); + + foreach (var value in values) + { + var valueStr = JObject.FromObject(value)? + .GetValue("Alphabetic", StringComparison.OrdinalIgnoreCase)? + .Value(); + + if (valueStr is not null) + { + resultStr.Add(valueStr); + } + } + + if (resultStr.Any() is true) + { + return string.Concat(resultStr); + } + + return string.Empty; + } + } +} diff --git a/src/InformaticsGateway/Services/Common/DicomValue.cs b/src/InformaticsGateway/Services/Common/DicomValue.cs new file mode 100755 index 000000000..b4d586efe --- /dev/null +++ b/src/InformaticsGateway/Services/Common/DicomValue.cs @@ -0,0 +1,44 @@ +/* + * 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.Text.Json.Serialization; + +namespace Monai.Deploy.InformaticsGateway.Services.Common +{ + public class DicomValue + { + [JsonPropertyName("vr")] + public string Vr { get; set; } = string.Empty; + + [JsonPropertyName("Value")] + public object[] Value { get; set; } = System.Array.Empty(); + } + + public static class DicomTagConstants + { + public const string PatientIdTag = "00100020"; + + public const string PatientNameTag = "00100010"; + + public const string PatientSexTag = "00100040"; + + public const string PatientDateOfBirthTag = "00100030"; + + public const string PatientAgeTag = "00101010"; + + public const string PatientHospitalIdTag = "00100021"; + } +} diff --git a/src/InformaticsGateway/Services/Common/IDicomService.cs b/src/InformaticsGateway/Services/Common/IDicomService.cs new file mode 100755 index 000000000..e39825e64 --- /dev/null +++ b/src/InformaticsGateway/Services/Common/IDicomService.cs @@ -0,0 +1,69 @@ +/* + * 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.Collections.Generic; +using System.Threading.Tasks; + +namespace Monai.Deploy.InformaticsGateway.Services.Common +{ + public interface IDicomService + { + /// + /// Gets a list of paths of all dicom images in the output directory. + /// + /// Output dir of the task. + /// Name of the bucket. + Task> GetDicomPathsForTaskAsync(string outputDirectory, string bucketName); + + /// + /// Gets patient details from the dicom metadata. + /// + /// Payload id. + /// Name of the bucket. + Task GetPayloadPatientDetailsAsync(string payloadId, string bucketName); + + /// + /// If series contains given value + /// if all values exist for given key example 0010 0040 then and + /// they are all same value return that value otherwise return + /// 'null' + /// + /// + /// + /// + /// + Task GetAllValueAsync(string keyId, string payloadId, string bucketId); + + /// + /// If any keyid exists return first occurance + /// if no matchs return 'null' + /// + /// example of keyId 00100040 + /// + /// + /// + Task GetAnyValueAsync(string keyId, string payloadId, string bucketId); + + /// + /// Gets value given DicomValue. + /// + /// + /// + /// + string GetValue(Dictionary dict, string keyId); + } +} diff --git a/src/InformaticsGateway/Services/Common/PatientDetails.cs b/src/InformaticsGateway/Services/Common/PatientDetails.cs new file mode 100755 index 000000000..87e925118 --- /dev/null +++ b/src/InformaticsGateway/Services/Common/PatientDetails.cs @@ -0,0 +1,48 @@ +/* + * 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.Text.Json; +using System.Text.Json.Serialization; + +namespace Monai.Deploy.InformaticsGateway.Services.Common +{ + public class PatientDetails + { + [JsonPropertyName("patient_id")] + public string? PatientId { get; set; } + + [JsonPropertyName("patient_name")] + public string? PatientName { get; set; } + + [JsonPropertyName("patient_sex")] + public string? PatientSex { get; set; } + + [JsonPropertyName("patient_dob")] + public DateTime? PatientDob { get; set; } + + [JsonPropertyName("patient_age")] + public string? PatientAge { get; set; } + + [JsonPropertyName("patient_hospital_id")] + public string? PatientHospitalId { get; set; } + + public override string ToString() + { + return JsonSerializer.Serialize(this); + } + } +} diff --git a/src/InformaticsGateway/Services/Connectors/PayloadAssembler.cs b/src/InformaticsGateway/Services/Connectors/PayloadAssembler.cs index 191867744..44aca203b 100755 --- a/src/InformaticsGateway/Services/Connectors/PayloadAssembler.cs +++ b/src/InformaticsGateway/Services/Connectors/PayloadAssembler.cs @@ -28,7 +28,9 @@ using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; using Monai.Deploy.InformaticsGateway.Logging; +using Monai.Deploy.InformaticsGateway.Services.Common; using Monai.Deploy.Messaging.Events; +using Monai.Deploy.Storage.API; #nullable enable @@ -48,10 +50,14 @@ internal sealed partial class PayloadAssembler : IPayloadAssembler, IDisposable private readonly Task _intializedTask; private readonly BlockingCollection _workItems; private readonly System.Timers.Timer _timer; + private readonly IDicomService _dicicomService; + private readonly string defaultBucket; public PayloadAssembler( ILogger logger, - IServiceScopeFactory serviceScopeFactory) + IServiceScopeFactory serviceScopeFactory + //,IOptions storageSettings + ) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); @@ -67,6 +73,12 @@ public PayloadAssembler( }; _timer.Elapsed += OnTimedEvent; _timer.Enabled = true; + + // defaultBucket = storageSettings.Value.bucketName + defaultBucket = "monaideploy"; + + var scope = _serviceScopeFactory.CreateScope(); + _dicicomService = scope.ServiceProvider.GetRequiredService(); } private async Task RemovePendingPayloads() @@ -143,6 +155,9 @@ private async void OnTimedEvent(Object source, System.Timers.ElapsedEventArgs e) { if (payload.IsUploadCompleted()) { + + var pd = await GetPatientDetails(payload); + _logger.ReceievedAPayload(payload.Elapsed.TotalSeconds, payload.Files.Count, payload.FilesFailedToUpload); if (_payloads.TryRemove(key, out _)) { @@ -213,6 +228,23 @@ private async Task CreateOrGetPayload(string key, string correlationId, })); } + private async Task GetPatientDetails(Payload payload) + { + try + { + var scope = _serviceScopeFactory.CreateScope(); + var storage = scope.ServiceProvider.GetRequiredService(); + return await _dicicomService.GetPayloadPatientDetailsAsync(payload.PayloadId.ToString(), defaultBucket).ConfigureAwait(false); + } + catch (Exception ex) + { + + var e = ex.Message; + return null; + } + + } + public void Dispose() { _payloads.Clear(); diff --git a/src/InformaticsGateway/Services/Connectors/PayloadNotificationActionHandler.cs b/src/InformaticsGateway/Services/Connectors/PayloadNotificationActionHandler.cs index ef8f8960f..f7319cbbc 100755 --- a/src/InformaticsGateway/Services/Connectors/PayloadNotificationActionHandler.cs +++ b/src/InformaticsGateway/Services/Connectors/PayloadNotificationActionHandler.cs @@ -93,9 +93,13 @@ private async Task DeletePayload(Payload payload, CancellationToken cancellation { Guard.Against.Null(payload, nameof(payload)); + // NDS removed delete, replaced with new status of complete. so we can query the payload data from WFM + // TODO add config and cleanup methods that run on background thread/process + var scope = _serviceScopeFactory.CreateScope(); var repository = scope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IPayloadRepository)); - await repository.RemoveAsync(payload, cancellationToken).ConfigureAwait(false); + payload.State = Payload.PayloadState.UploadComplete; + await repository.UpdateAsync(payload, cancellationToken).ConfigureAwait(false); } private async Task NotifyPayloadReady(Payload payload) @@ -187,4 +191,4 @@ public void Dispose() GC.SuppressFinalize(this); } } -} \ No newline at end of file +} diff --git a/src/InformaticsGateway/Services/Http/PayloadController.cs b/src/InformaticsGateway/Services/Http/PayloadController.cs new file mode 100755 index 000000000..237b55037 --- /dev/null +++ b/src/InformaticsGateway/Services/Http/PayloadController.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Logging; +using Monai.Deploy.InformaticsGateway.Services.Storage; + +namespace Monai.Deploy.InformaticsGateway.Services.Http +{ + [ApiController] + [Route("payload")] + public class PayloadController : ControllerBase + { + private readonly ILogger _logger; + private readonly IPayloadRepository _repository; + + + public PayloadController( + ILogger logger, + IPayloadRepository 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 + { + var payloads = await _repository.ToListAsync(HttpContext.RequestAborted).ConfigureAwait(false); + return Ok(payloads.Select(payload => new PayloadDTO(payload))); + } + catch (Exception ex) + { + _logger.ErrorQueryingDatabase(ex); + return Problem(title: "Error querying database.", statusCode: (int)System.Net.HttpStatusCode.InternalServerError, detail: ex.Message); + } + } + } +} diff --git a/src/InformaticsGateway/Services/Scp/ScpServiceInternal.cs b/src/InformaticsGateway/Services/Scp/ScpServiceInternal.cs old mode 100644 new mode 100755 index 6caedf44e..cd650dcae --- a/src/InformaticsGateway/Services/Scp/ScpServiceInternal.cs +++ b/src/InformaticsGateway/Services/Scp/ScpServiceInternal.cs @@ -170,6 +170,7 @@ await SendAssociationRejectAsync( DicomRejectResult.Permanent, DicomRejectSource.ServiceUser, DicomRejectReason.CallingAENotRecognized).ConfigureAwait(false); + _logger.ScuAssociationRejected(); } if (!await IsValidCalledAeAsync(association.CalledAE).ConfigureAwait(false)) @@ -180,6 +181,7 @@ await SendAssociationRejectAsync( DicomRejectResult.Permanent, DicomRejectSource.ServiceUser, DicomRejectReason.CalledAENotRecognized).ConfigureAwait(false); + _logger.ScuAssociationRejected(); } foreach (var pc in association.PresentationContexts) diff --git a/src/InformaticsGateway/Services/Storage/ObjectUploadService.cs b/src/InformaticsGateway/Services/Storage/ObjectUploadService.cs old mode 100644 new mode 100755 index 88c36fc3f..0a0d84448 --- a/src/InformaticsGateway/Services/Storage/ObjectUploadService.cs +++ b/src/InformaticsGateway/Services/Storage/ObjectUploadService.cs @@ -18,9 +18,12 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Text; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -33,6 +36,7 @@ using Monai.Deploy.InformaticsGateway.Logging; using Monai.Deploy.InformaticsGateway.Services.Common; using Monai.Deploy.Storage.API; +using Newtonsoft.Json; using Polly; namespace Monai.Deploy.InformaticsGateway.Services.Storage @@ -159,6 +163,12 @@ private async Task ProcessObject(int thread, FileStorageMetadata blob) if (!string.IsNullOrWhiteSpace(dicom.JsonFile.TemporaryPath)) { await UploadFileAndConfirm(dicom.Id, dicom.JsonFile, dicom.DataOrigin.Source, dicom.Workflows, blob.PayloadId, _cancellationTokenSource.Token).ConfigureAwait(false); + var jsonstream = dicom.JsonFile.Data; + var jsonStr = Encoding.UTF8.GetString(((MemoryStream)jsonstream).ToArray()); + + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + JsonConvert.PopulateObject(jsonStr, dict); + var test = 0; } break; } diff --git a/src/InformaticsGateway/Services/Storage/PayloadDTO.cs b/src/InformaticsGateway/Services/Storage/PayloadDTO.cs new file mode 100755 index 000000000..746e2f0fe --- /dev/null +++ b/src/InformaticsGateway/Services/Storage/PayloadDTO.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Monai.Deploy.InformaticsGateway.Api.Storage; +using Monai.Deploy.Messaging.Events; +using static Monai.Deploy.InformaticsGateway.Api.Storage.Payload; + +namespace Monai.Deploy.InformaticsGateway.Services.Storage +{ + public class PayloadDTO + { + public PayloadDTO(Payload payload) + { + PayloadId = payload.PayloadId; + Key = payload.Key; + MachineName = payload.MachineName; + DateTimeCreated = payload.DateTimeCreated; + State = payload.State; + Files = payload.Files.Select(f => new FileInfo(f)).ToList(); + CorrelationId = payload.CorrelationId; + WorkflowInstanceId = payload.WorkflowInstanceId; + TaskId = payload.TaskId; + DataTrigger = payload.DataTrigger; + Count = payload.Count; + FilesUploaded = payload.FilesUploaded; + FilesFailedToUpload = payload.FilesFailedToUpload; + } + + public Guid PayloadId { get; private set; } + + public uint Timeout { get; init; } + + public string Key { get; init; } + + public string? MachineName { get; init; } + + public DateTime DateTimeCreated { get; private set; } + + public PayloadState State { get; set; } + + public List Files { get; init; } + + public string CorrelationId { get; init; } + + public string? WorkflowInstanceId { get; init; } + + public string? TaskId { get; init; } + + public DataOrigin DataTrigger { get; init; } + + public HashSet DataOrigins { get; init; } + + public int Count; + + public bool HasTimedOut; + + public int FilesUploaded; + + public int FilesFailedToUpload; + } + + public class FileInfo + { + public FileInfo(FileStorageMetadata meta) + { + FileName = meta.File.UploadPath; + FileStatus = "Pending"; + if (meta.File.IsMoveCompleted) + { + FileStatus = "Complete"; + } + if (meta.File.IsUploadFailed) + { + FileStatus = "Failed"; + } + } + + public string FileName { get; set; } + + public string FileStatus { get; set; } + } +}