Skip to content

Commit 64264fc

Browse files
authored
Implement FHIR server (#118)
* gh-29 Implement FHIR server * gh-29 Unit test for FHIR service * gh-29 Test feature for FHIR * Update API doc & changelog Signed-off-by: Victor Chang <[email protected]>
1 parent 3e85792 commit 64264fc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+6293
-56
lines changed

.github/.gitversion.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
next-version: 0.2.0
1516
assembly-versioning-scheme: MajorMinorPatchTag
1617
mode: ContinuousDelivery
1718
branches:

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ jobs:
205205
needs: [build]
206206
strategy:
207207
matrix:
208-
feature: [AcrApi, DicomDimseScp, DicomDimseScu, DicomWebExport, DicomWebStow, HealthLevel7]
208+
feature: [AcrApi, DicomDimseScp, DicomDimseScu, DicomWebExport, DicomWebStow, HealthLevel7, Fhir]
209209
fail-fast: false
210210
env:
211211
TAG: ${{ needs.build.outputs.TAG }}

.licenserc.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ header:
3333
- 'demos/**/.env/**'
3434
- 'docs/templates/**'
3535
- 'tests/Integration.Test/*.dev'
36+
- 'tests/Integration.Test/data/**'
3637

3738
comment: on-failure
3839

.vscode/launch.json

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,11 @@
1212
"type": "coreclr",
1313
"request": "launch",
1414
"preLaunchTask": "build-cli",
15-
"program": "${workspaceFolder}/src/CLI/bin/Debug/net5.0/linux-x64/mig-cli",
15+
"program": "${workspaceFolder}/src/CLI/bin/Debug/net6.0/linux-x64/mig-cli",
1616
"args": [
17-
"config",
18-
"endpoint",
19-
"http://localhost:4500"
17+
"start"
2018
],
21-
"cwd": "${workspaceFolder}/src/CLI/bin/Debug/net5.0/linux-x64",
19+
"cwd": "${workspaceFolder}/src/CLI/bin/Debug/net6.0/linux-x64",
2220
"stopAtEntry": true,
2321
"console": "internalConsole"
2422
},

docs/api/rest/fhir.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<!--
2+
~ Copyright 2022 MONAI Consortium
3+
~
4+
~ Licensed under the Apache License, Version 2.0 (the "License");
5+
~ you may not use this file except in compliance with the License.
6+
~ You may obtain a copy of the License at
7+
~
8+
~ http://www.apache.org/licenses/LICENSE-2.0
9+
~
10+
~ Unless required by applicable law or agreed to in writing, software
11+
~ distributed under the License is distributed on an "AS IS" BASIS,
12+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
~ See the License for the specific language governing permissions and
14+
~ limitations under the License.
15+
-->
16+
17+
# DICOMWeb STOW-RS APIs
18+
19+
The `fhir/` endpoint implements the specifications defined in [section 3.1.0 RESTful API](http://hl7.org/implement/standards/fhir/http.html)
20+
defined by HL7 (Health Level 7 International) to enable triggering new workflows. The FHIR service supports multiple versions of the Fast Healthcare Interoperability Resources (FHIR) specifications published by Health Level 7 International (HL7).
21+
22+
[!Note]
23+
The service does not support `CapabilityStatement` at this moment and does not group the incoming FHIR resources. Therefore, each incoming FHIR resource will trigger a new workflow request.
24+
25+
The *FHIR* service provides the following endpoint.
26+
27+
## POST /fhir/[resource]
28+
29+
Triggers a new workflow request with the posted FHIR resource.
30+
31+
> [!IMPORTANT]
32+
> Refer to [section 1.2 Resource Index](http://hl7.org/fhir/resourcelist.html) for a list of HL7 resources. The endpoint is designed to accept any resource and provides only syntax validation either in XML or JSON.
33+
34+
### Parameters
35+
36+
#### Query Parameters:
37+
38+
| Name | Type | Description |
39+
| -------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
40+
| resource | string | (Optional) resouce type of the FHIR document. The services reject the request if the `Resource` value in the URL differs from the `Resource` value described in the posted document. |
41+
42+
#### Request Body:
43+
44+
Supported Content-Types:
45+
46+
- `application/fhir+json`
47+
- `application/fhir+xml`
48+
49+
### Responses
50+
51+
Depending on the `Accept` header or the original document, the response supports the following content types:
52+
53+
- `application/fhir+json`
54+
- `application/fhir+xml`
55+
56+
If the `Accept` header is missing or a none supported value exists, the service will return the same type as the posted document.
57+
58+
| Code | Data Type | Description |
59+
| ---- | ------------------------------------------------------------- | --------------------------------------------------------------------- |
60+
| 201 | Original JSON or XML document. | Resource created & stored successfully. |
61+
| 400 | [OperationOutcome](http://hl7.org/fhir/operationoutcome.html) | Unable to parse the resource or mismatching resource type specified.. |
62+
| 415 | `none` | Unsupported media type |
63+
| 500 | [OperationOutcome](http://hl7.org/fhir/operationoutcome.html) | Server error. |
64+
65+
[!Note]
66+
The `Location` header in the response given that the resources created are for inference only.

docs/changelog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616

1717

1818
# Changelog
19+
## 0.3.0
20+
21+
[GitHub Milestone 0.3.0](https://github.com/Project-MONAI/monai-deploy-informatics-gateway/milestone/3)
22+
23+
- Adds a basic [FHIR service](api/rest/config.md) to accept any versions of FHIR.
1924

2025
## 0.2.0
2126

src/CLI/Services/DockerRunner.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,6 @@ public async Task<bool> StartApplication(ImageVersion imageVersion, Cancellation
135135
_fileSystem.Directory.CreateDirectoryIfNotExists(_configurationService.Configurations.HostDatabaseStorageMount);
136136
createContainerParams.HostConfig.Mounts.Add(new Mount { Type = "bind", ReadOnly = false, Source = _configurationService.Configurations.HostDatabaseStorageMount, Target = Common.MountedDatabasePath });
137137

138-
_logger.DockerMountTempDirectory(_configurationService.Configurations.HostDataStorageMount, _configurationService.Configurations.TempStoragePath);
139-
_fileSystem.Directory.CreateDirectoryIfNotExists(_configurationService.Configurations.HostDataStorageMount);
140-
createContainerParams.HostConfig.Mounts.Add(new Mount { Type = "bind", ReadOnly = false, Source = _configurationService.Configurations.HostDataStorageMount, Target = _configurationService.Configurations.TempStoragePath });
141-
142138
_logger.DockerMountAppLogs(_configurationService.Configurations.HostLogsStorageMount, _configurationService.Configurations.LogStoragePath);
143139
_fileSystem.Directory.CreateDirectoryIfNotExists(_configurationService.Configurations.HostLogsStorageMount);
144140
createContainerParams.HostConfig.Mounts.Add(new Mount { Type = "bind", ReadOnly = false, Source = _configurationService.Configurations.HostLogsStorageMount, Target = _configurationService.Configurations.LogStoragePath });

src/CLI/Test/DockerRunnerTest.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,6 @@ public async Task StartApplication()
176176
c.ExposedPorts.ContainsKey("200/tcp") &&
177177
c.HostConfig.Mounts.Count(m => m.ReadOnly && m.Source == Common.ConfigFilePath && m.Target == Common.MountedConfigFilePath) == 1 &&
178178
c.HostConfig.Mounts.Count(m => !m.ReadOnly && m.Source == "/database" && m.Target == Common.MountedDatabasePath) == 1 &&
179-
c.HostConfig.Mounts.Count(m => !m.ReadOnly && m.Source == "/storage" && m.Target == "/tempdata") == 1 &&
180179
c.HostConfig.Mounts.Count(m => !m.ReadOnly && m.Source == "/logs" && m.Target == "/templogs") == 1), It.IsAny<CancellationToken>()), Times.Exactly(2));
181180

182181
_logger.VerifyLogging("Warnings: warning1", LogLevel.Warning, Times.Once());

src/Database/Test/StorageMetadataWrapperTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public void GivenAFhirFileStorageMetadataObject_WhenInitializedWithStorageMetada
3838

3939
var unwrapped = wrapper.GetObject() as FhirFileStorageMetadata;
4040

41-
Assert.Equal(metadata.CorrelationId, unwrapped.CorrelationId);
41+
Assert.Equal(metadata.CorrelationId, unwrapped!.CorrelationId);
4242
Assert.Equal(metadata.Id, unwrapped.Id);
4343
Assert.Equal(metadata.DataTypeDirectoryName, unwrapped.DataTypeDirectoryName);
4444
Assert.Equal(metadata.DateReceived, unwrapped.DateReceived);
@@ -79,7 +79,7 @@ public void GivenADicomFileStorageMetadataObject_WhenInitializedWithStorageMetad
7979

8080
var unwrapped = wrapper.GetObject() as DicomFileStorageMetadata;
8181

82-
Assert.Equal(metadata.CalledAeTitle, unwrapped.CalledAeTitle);
82+
Assert.Equal(metadata.CalledAeTitle, unwrapped!.CalledAeTitle);
8383
Assert.Equal(metadata.CallingAeTitle, unwrapped.CallingAeTitle);
8484
Assert.Equal(metadata.CorrelationId, unwrapped.CorrelationId);
8585
Assert.Equal(metadata.Id, unwrapped.Id);

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,29 @@ public static partial class Log
112112
public static partial void StowFailedWithNoSpace(this ILogger logger, Exception ex = null);
113113

114114
[LoggerMessage(EventId = 8108, Level = LogLevel.Information, Message = "STOW instance queued.")]
115-
public static partial void QueuedInstanceUsingCorrelationId(this ILogger logger);
115+
public static partial void QueuedStowInstance(this ILogger logger);
116116

117117
[LoggerMessage(EventId = 8109, Level = LogLevel.Information, Message = "Saving {count} DICOMWeb STOW-RS streams.")]
118118
public static partial void SavingStream(this ILogger logger, int count);
119119

120120
[LoggerMessage(EventId = 8110, Level = LogLevel.Warning, Message = "Ignoring zero length stream.")]
121121
public static partial void ZeroLengthDicomWebStowStream(this ILogger logger);
122+
123+
// FHIR Serer
124+
125+
[LoggerMessage(EventId = 8200, Level = LogLevel.Debug, Message = "Parsing FHIR as JSON.")]
126+
public static partial void ParsingFhirJson(this ILogger logger);
127+
128+
[LoggerMessage(EventId = 8201, Level = LogLevel.Debug, Message = "Parsing FHIR as XML.")]
129+
public static partial void ParsingFhirXml(this ILogger logger);
130+
131+
[LoggerMessage(EventId = 8202, Level = LogLevel.Information, Message = "FHIR instance queued.")]
132+
public static partial void QueueFhirInstance(this ILogger logger);
133+
134+
[LoggerMessage(EventId = 8203, Level = LogLevel.Error, Message = "Error storing FHIR object.")]
135+
public static partial void FhirStoreException(this ILogger logger, Exception ex);
136+
137+
[LoggerMessage(EventId = 8204, Level = LogLevel.Error, Message = "Failed to store FHIR resource.")]
138+
public static partial void ErrorStoringFhirResource(this ILogger logger, Exception ex);
122139
}
123140
}

src/InformaticsGateway/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
using Monai.Deploy.InformaticsGateway.Services.Connectors;
3535
using Monai.Deploy.InformaticsGateway.Services.DicomWeb;
3636
using Monai.Deploy.InformaticsGateway.Services.Export;
37+
using Monai.Deploy.InformaticsGateway.Services.Fhir;
3738
using Monai.Deploy.InformaticsGateway.Services.HealthLevel7;
3839
using Monai.Deploy.InformaticsGateway.Services.Http;
3940
using Monai.Deploy.InformaticsGateway.Services.Scp;
@@ -103,6 +104,7 @@ internal static IHostBuilder CreateHostBuilder(string[] args) =>
103104
services.AddTransient<IFileSystem, FileSystem>();
104105
services.AddTransient<IDicomToolkit, DicomToolkit>();
105106
services.AddTransient<IStowService, StowService>();
107+
services.AddTransient<IFhirService, FhirService>();
106108
services.AddTransient<IStreamsWriter, StreamsWriter>();
107109
services.AddTransient<IApplicationEntityHandler, ApplicationEntityHandler>();
108110

src/InformaticsGateway/Services/DicomWeb/IStreamsWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ private async Task SaveInstance(Stream stream, string studyInstanceUid, string w
170170

171171
// for DICOMweb, use correlation ID as the grouping key
172172
await _payloadAssembler.Queue(correlationId, dicomInfo, _configuration.Value.DicomWeb.Timeout).ConfigureAwait(false);
173-
_logger.QueuedInstanceUsingCorrelationId();
173+
_logger.QueuedStowInstance();
174174

175175
AddSuccess(null, uids);
176176

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2022 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
namespace Monai.Deploy.InformaticsGateway.Services.Fhir
18+
{
19+
internal static class ContentTypes
20+
{
21+
public const string ApplicationFhirXml = "application/fhir+xml";
22+
public const string ApplicationFhirJson = "application/fhir+json";
23+
}
24+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2022 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System.Text.Json;
18+
using Microsoft.AspNetCore.Mvc.Formatters;
19+
20+
namespace Monai.Deploy.InformaticsGateway.Services.Fhir
21+
{
22+
internal class FhirJsonFormatters : SystemTextJsonOutputFormatter
23+
{
24+
public FhirJsonFormatters(JsonSerializerOptions jsonSerializerOptions) : base(jsonSerializerOptions)
25+
{
26+
SupportedMediaTypes.Clear();
27+
SupportedMediaTypes.Add(ContentTypes.ApplicationFhirJson);
28+
}
29+
}
30+
31+
internal class FhirXmlFormatters : XmlDataContractSerializerOutputFormatter
32+
33+
{
34+
public FhirXmlFormatters()
35+
{
36+
SupportedMediaTypes.Clear();
37+
SupportedMediaTypes.Add(ContentTypes.ApplicationFhirXml);
38+
}
39+
}
40+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2022 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System;
18+
using System.IO;
19+
using System.Text.Json;
20+
using System.Text.Json.Nodes;
21+
using System.Threading;
22+
using System.Threading.Tasks;
23+
using Ardalis.GuardClauses;
24+
using Microsoft.AspNetCore.Http;
25+
using Microsoft.Extensions.Logging;
26+
using Microsoft.Net.Http.Headers;
27+
using Monai.Deploy.InformaticsGateway.Api.Storage;
28+
using Monai.Deploy.InformaticsGateway.Common;
29+
using Monai.Deploy.InformaticsGateway.Logging;
30+
31+
namespace Monai.Deploy.InformaticsGateway.Services.Fhir
32+
{
33+
internal class FhirJsonReader : IFHirRequestReader
34+
{
35+
private readonly ILogger<FhirJsonReader> _logger;
36+
37+
public FhirJsonReader(ILogger<FhirJsonReader> logger)
38+
{
39+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
40+
}
41+
42+
public async Task<FhirStoreResult> GetContentAsync(HttpRequest request, string correlationId, string resourceType, MediaTypeHeaderValue mediaTypeHeaderValue, CancellationToken cancellationToken)
43+
{
44+
Guard.Against.Null(request, nameof(request));
45+
Guard.Against.NullOrWhiteSpace(correlationId, nameof(correlationId));
46+
Guard.Against.NullOrInvalidInput(mediaTypeHeaderValue, nameof(mediaTypeHeaderValue), (value) =>
47+
{
48+
return value.MediaType.Value.Equals(ContentTypes.ApplicationFhirJson, StringComparison.OrdinalIgnoreCase);
49+
});
50+
51+
_logger.ParsingFhirJson();
52+
53+
var result = new FhirStoreResult
54+
{
55+
ResourceType = resourceType,
56+
RawData = await new StreamReader(request.Body).ReadToEndAsync().ConfigureAwait(false)
57+
};
58+
59+
var jsonDoc = JsonNode.Parse(result.RawData);
60+
61+
if (jsonDoc[Resources.PropertyResourceType] is not null)
62+
{
63+
result.InternalResourceType = jsonDoc[Resources.PropertyResourceType].GetValue<string>();
64+
}
65+
66+
var resourceId = SetIdIfMIssing(correlationId, jsonDoc);
67+
68+
result.RawData = jsonDoc.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
69+
70+
var fileMetadata = new FhirFileStorageMetadata(correlationId, result.InternalResourceType, resourceId, Api.Rest.FhirStorageFormat.Json);
71+
fileMetadata.SetDataStream(result.RawData);
72+
73+
result.Metadata = fileMetadata;
74+
return result;
75+
}
76+
77+
private static string SetIdIfMIssing(string correlationId, JsonNode jsonDoc)
78+
{
79+
Guard.Against.NullOrWhiteSpace(correlationId, nameof(correlationId));
80+
Guard.Against.Null(jsonDoc, nameof(jsonDoc));
81+
82+
if (string.IsNullOrWhiteSpace(jsonDoc[Resources.PropertyId]?.GetValue<string>()))
83+
{
84+
jsonDoc[Resources.PropertyId] = correlationId;
85+
}
86+
87+
return jsonDoc[Resources.PropertyId].GetValue<string>();
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)