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