Skip to content

Commit

Permalink
Story/672/marshal results into fhir (#755)
Browse files Browse the repository at this point in the history
* Initial commit with the needed FHIR resources created in the HapiOrderConverter

* Updating Orchestrator with WIP of converter call

* Fixed postgres DAO issues and added marshaling to metadata handler

* Reverting Azure client registration

* Fixing other test

* Fixing test

* Fixing formatting

* Adding test for Domain Response helper methods

* Re-adding constructor temporarily

* Fixing e2e test and linting

* Removing commented code

* Updated openapi docs for metadata response

* Adding wrapper class

* Adding java docs

---------

Co-authored-by: Basilio Bogado <[email protected]>
Co-authored-by: halprin <[email protected]>
  • Loading branch information
3 people authored Jan 9, 2024
1 parent b368cec commit 921f82a
Show file tree
Hide file tree
Showing 16 changed files with 166 additions and 34 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/terraform-deploy_reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:
- name: Run Db migration
run: |
export PGPASSWORD=$(az account get-access-token --resource-type oss-rdbms --query "[accessToken]" -o tsv)
psql "host=$(terraform output -raw database_hostname) port=5432 dbname=postgres user=cdcti-github sslmode=require" -c "CREATE TABLE IF NOT EXISTS metadata (message_id varchar(30), sender varchar(30), receiver varchar(30), hash_of_order varchar(1000), time_received timestamptz); GRANT ALL ON metadata TO azure_pg_admin; ALTER TABLE metadata OWNER TO azure_pg_admin;"
psql "host=$(terraform output -raw database_hostname) port=5432 dbname=postgres user=cdcti-github sslmode=require" -c "CREATE TABLE IF NOT EXISTS metadata (message_id varchar(30) PRIMARY KEY, sender varchar(30), receiver varchar(30), hash_of_order varchar(1000), time_received timestamptz); GRANT ALL ON metadata TO azure_pg_admin; ALTER TABLE metadata OWNER TO azure_pg_admin;"
- id: export-terraform-output
name: Export Terraform Output
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class MetadataTest extends Specification {

then:
metadataResponse.getCode() == expectedStatusCode
parsedJsonBody.receivedSubmissionId == submissionId
parsedJsonBody.get("id") == submissionId
}

def "a 404 is returned when there is no metadata for a given ID"() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadataException;
import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadataOrchestrator;
import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadataStorage;
import gov.hhs.cdc.trustedintermediary.etor.operationoutcomes.FhirMetadata;
import gov.hhs.cdc.trustedintermediary.etor.orders.Order;
import gov.hhs.cdc.trustedintermediary.etor.orders.OrderController;
import gov.hhs.cdc.trustedintermediary.etor.orders.OrderConverter;
Expand All @@ -34,6 +35,7 @@
import gov.hhs.cdc.trustedintermediary.external.reportstream.ReportStreamOrderSender;
import gov.hhs.cdc.trustedintermediary.wrappers.DbDao;
import gov.hhs.cdc.trustedintermediary.wrappers.FhirParseException;
import gov.hhs.cdc.trustedintermediary.wrappers.HapiFhir;
import gov.hhs.cdc.trustedintermediary.wrappers.Logger;
import gov.hhs.cdc.trustedintermediary.wrappers.SqlDriverManager;
import java.io.IOException;
Expand Down Expand Up @@ -62,6 +64,10 @@ public class EtorDomainRegistration implements DomainConnector {
@Inject DomainResponseHelper domainResponseHelper;
@Inject PartnerMetadataOrchestrator partnerMetadataOrchestrator;

@Inject OrderConverter orderConverter;

@Inject HapiFhir fhir;

private final Map<HttpEndpoint, Function<DomainRequest, DomainResponse>> endpoints =
Map.of(
new HttpEndpoint("POST", DEMOGRAPHICS_API_ENDPOINT, true),
Expand Down Expand Up @@ -175,7 +181,11 @@ DomainResponse handleMetadata(DomainRequest request) {
404, "Metadata not found for ID: " + metadataId);
}

return domainResponseHelper.constructOkResponse(metadata.get());
FhirMetadata<?> responseObject =
orderConverter.extractPublicMetadataToOperationOutcome(metadata.get());

return domainResponseHelper.constructOkResponseFromString(
fhir.encodeResourceToJson(responseObject.getUnderlyingOutcome()));
} catch (PartnerMetadataException e) {
String errorMessage = "Unable to retrieve requested metadata";
logger.logError(errorMessage, e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ public PartnerMetadata(
this(receivedSubmissionId, null, sender, null, timeReceived, hash);
}

public PartnerMetadata(
String receivedSubmissionId,
String sender,
String receiver,
Instant timeReceived,
String hash) {
this(receivedSubmissionId, null, sender, receiver, timeReceived, hash);
}

public PartnerMetadata(String receivedSubmissionId, String hash) {
this(receivedSubmissionId, null, null, null, null, hash);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package gov.hhs.cdc.trustedintermediary.etor.operationoutcomes;

/**
* Wrapper interface for our public facing metadata. Wraps an operation outcomes object to be
* returned to our ReST API
*
* @param <T>
*/
public interface FhirMetadata<T> {

T getUnderlyingOutcome();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package gov.hhs.cdc.trustedintermediary.etor.operationoutcomes;

import org.hl7.fhir.r4.model.OperationOutcome;

/** Implementation of our wrapper for public facing metadata. Returns an operation outcomes */
public class HapiFhirMetadata implements FhirMetadata<OperationOutcome> {

private final OperationOutcome innerOutcome;

public HapiFhirMetadata(OperationOutcome outcome) {
this.innerOutcome = outcome;
}

@Override
public OperationOutcome getUnderlyingOutcome() {
return innerOutcome;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package gov.hhs.cdc.trustedintermediary.etor.orders;

import gov.hhs.cdc.trustedintermediary.etor.demographics.Demographics;
import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadata;
import gov.hhs.cdc.trustedintermediary.etor.operationoutcomes.FhirMetadata;

/** Interface for converting things to orders and things in orders. */
public interface OrderConverter {
Expand All @@ -9,4 +11,6 @@ public interface OrderConverter {
Order<?> convertMetadataToOmlOrder(Order<?> order);

Order<?> addContactSectionToPatientResource(Order<?> order);

FhirMetadata<?> extractPublicMetadataToOperationOutcome(PartnerMetadata metadata);
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ protected Connection connect() throws SQLException {

// If the below prop isn't set to require and we just set ssl=true it will expect a CA cert
// in azure which breaks it
props.setProperty("sslmode", ssl);
props.setProperty("ssl", ssl);
conn = driverManager.getConnection(url, props);
logger.logInfo("DB Connected Successfully");
return conn;
Expand All @@ -90,9 +90,12 @@ public synchronized void upsertMetadata(

try (Connection conn = connect();
PreparedStatement statement =
conn.prepareStatement("INSERT INTO metadata VALUES (?, ?, ?, ?, ?)")) {
// TODO: Update the below statement to handle on conflict, after we figure out what that
// behavior should be
conn.prepareStatement(
"""
INSERT INTO metadata VALUES (?, ?, ?, ?, ?)
ON CONFLICT (message_id) DO UPDATE SET receiver = EXCLUDED.receiver
""")) {

statement.setString(1, receivedSubmissionId);
statement.setString(2, sender);
statement.setString(3, receiver);
Expand Down Expand Up @@ -120,6 +123,7 @@ public synchronized PartnerMetadata fetchMetadata(String receivedSubmissionId)

return new PartnerMetadata(
result.getString("message_id"),
result.getString("sender"),
result.getString("receiver"),
result.getTimestamp("time_received").toInstant(),
result.getString("hash_of_order"));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package gov.hhs.cdc.trustedintermediary.external.hapi;

import gov.hhs.cdc.trustedintermediary.etor.demographics.Demographics;
import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadata;
import gov.hhs.cdc.trustedintermediary.etor.operationoutcomes.FhirMetadata;
import gov.hhs.cdc.trustedintermediary.etor.operationoutcomes.HapiFhirMetadata;
import gov.hhs.cdc.trustedintermediary.etor.orders.Order;
import gov.hhs.cdc.trustedintermediary.etor.orders.OrderConverter;
import gov.hhs.cdc.trustedintermediary.wrappers.Logger;
Expand All @@ -15,6 +18,7 @@
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.MessageHeader;
import org.hl7.fhir.r4.model.Meta;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Provenance;
import org.hl7.fhir.r4.model.Reference;
Expand Down Expand Up @@ -200,4 +204,36 @@ private Provenance createProvenanceResource(Date orderDate) {

return provenance;
}

@Override
public FhirMetadata<?> extractPublicMetadataToOperationOutcome(PartnerMetadata metadata) {
var operation = new OperationOutcome();

operation.setId(metadata.receivedSubmissionId());
operation.getIssue().add(createInformationIssueComponent("sender name", metadata.sender()));
operation
.getIssue()
.add(createInformationIssueComponent("receiver name", metadata.receiver()));
operation
.getIssue()
.add(
createInformationIssueComponent(
"order ingestion", metadata.timeReceived().toString()));
operation.getIssue().add(createInformationIssueComponent("payload hash", metadata.hash()));

return new HapiFhirMetadata(operation);
}

protected OperationOutcome.OperationOutcomeIssueComponent createInformationIssueComponent(
String details, String diagnostics) {
OperationOutcome.OperationOutcomeIssueComponent issue =
new OperationOutcome.OperationOutcomeIssueComponent();

issue.setSeverity(OperationOutcome.IssueSeverity.INFORMATION);
issue.setCode(OperationOutcome.IssueType.INFORMATIONAL);
issue.getDetails().setText(details);
issue.setDiagnostics(diagnostics);

return issue;
}
}
20 changes: 1 addition & 19 deletions etor/src/main/resources/openapi_etor.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/MetadataResponse'
$ref: 'https://github.com/LinuxForHealth/FHIR/blob/main/fhir-openapi/src/main/webapp/META-INF/openapi.json?raw=true#/components/schemas/OperationOutcome'
'401':
description: Authentication failed due to invalid token or unknown organization
content:
Expand Down Expand Up @@ -125,24 +125,6 @@ components:
patientId:
type: string
example: MRN7465737865
MetadataResponse:
type: object
properties:
uniqueId:
type: string
example: abc123
sender:
type: string
example: simulated-hospital
receiver:
type: string
example: simulated-lab
timeReceived:
type: string
example: 2023-12-01T12:00:00.000000Z
hash:
type: string
example: abc123
BadRequestError:
description: Bad Request
type: object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@ import gov.hhs.cdc.trustedintermediary.etor.demographics.PatientDemographicsResp
import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadata
import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadataException
import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadataOrchestrator
import gov.hhs.cdc.trustedintermediary.etor.operationoutcomes.FhirMetadata
import gov.hhs.cdc.trustedintermediary.etor.orders.Order
import gov.hhs.cdc.trustedintermediary.etor.orders.OrderController
import gov.hhs.cdc.trustedintermediary.etor.orders.OrderConverter
import gov.hhs.cdc.trustedintermediary.etor.orders.OrderResponse
import gov.hhs.cdc.trustedintermediary.etor.orders.SendOrderUseCase
import gov.hhs.cdc.trustedintermediary.etor.orders.UnableToSendOrderException
import gov.hhs.cdc.trustedintermediary.external.jackson.Jackson
import gov.hhs.cdc.trustedintermediary.wrappers.FhirParseException
import gov.hhs.cdc.trustedintermediary.wrappers.HapiFhir
import gov.hhs.cdc.trustedintermediary.wrappers.Logger
import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter
import org.hl7.fhir.r4.model.OperationOutcome

import java.time.Instant
import spock.lang.Specification

Expand Down Expand Up @@ -325,10 +328,16 @@ class EtorDomainRegistrationTest extends Specification {
TestApplicationContext.register(PartnerMetadataOrchestrator, mockPartnerMetadataOrchestrator)

def mockResponseHelper = Mock(DomainResponseHelper)
mockResponseHelper.constructOkResponse(_ as PartnerMetadata) >> new DomainResponse(expectedStatusCode)
mockResponseHelper.constructOkResponseFromString(_ as String) >> new DomainResponse(expectedStatusCode)
TestApplicationContext.register(DomainResponseHelper, mockResponseHelper)

TestApplicationContext.register(Formatter, Jackson.getInstance())
def mockOrderConverter = Mock(OrderConverter)
mockOrderConverter.extractPublicMetadataToOperationOutcome(_ as PartnerMetadata) >> Mock(FhirMetadata)
TestApplicationContext.register(OrderConverter, mockOrderConverter)

def mockFhir = Mock(HapiFhir)
mockFhir.encodeResourceToJson(_) >> ""
TestApplicationContext.register(HapiFhir, mockFhir)
TestApplicationContext.injectRegisteredImplementations()

when:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package gov.hhs.cdc.trustedintermediary.etor.metadata

import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext
import gov.hhs.cdc.trustedintermediary.etor.RSEndpointClient
import gov.hhs.cdc.trustedintermediary.etor.orders.Order
import gov.hhs.cdc.trustedintermediary.etor.orders.OrderConverter
import gov.hhs.cdc.trustedintermediary.external.hapi.HapiOrderConverter
import gov.hhs.cdc.trustedintermediary.external.jackson.Jackson
import gov.hhs.cdc.trustedintermediary.external.reportstream.ReportStreamEndpointClientException
import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter
Expand All @@ -25,9 +28,12 @@ class PartnerMetadataOrchestratorTest extends Specification {
mockClient = Mock(RSEndpointClient)

TestApplicationContext.register(PartnerMetadataOrchestrator, PartnerMetadataOrchestrator.getInstance())
TestApplicationContext.register(OrderConverter, HapiOrderConverter.getInstance())
TestApplicationContext.register(PartnerMetadataStorage, mockPartnerMetadataStorage)

TestApplicationContext.register(RSEndpointClient, mockClient)
TestApplicationContext.register(Formatter, mockFormatter)

TestApplicationContext.injectRegisteredImplementations()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,17 +148,18 @@ class PostgresDaoTest extends Specification {
def "fetchMetadata returns partnermetadata when rows exist"() {
given:
def messageId = "12345"
def receiver = "DogCow"
def sender = "DogCow"
Timestamp timestampForMock = Timestamp.from(Instant.parse("2024-01-03T15:45:33.30Z"))
Instant timeReceived = timestampForMock.toInstant()
def hash = receiver.hashCode().toString()
def expected = new PartnerMetadata(messageId, receiver, timeReceived, hash)
def hash = sender.hashCode().toString()
def expected = new PartnerMetadata(messageId, sender, null, timeReceived, hash)

mockDriver.getConnection(_ as String, _ as Properties) >> mockConn
mockConn.prepareStatement(_ as String) >> mockPreparedStatement
mockResultSet.next() >> true
mockResultSet.getString("message_id") >> messageId
mockResultSet.getString("receiver") >> receiver
mockResultSet.getString("sender") >> sender
mockResultSet.getString("receiver") >> null
mockResultSet.getTimestamp("time_received") >> timestampForMock
mockResultSet.getString("hash_of_order") >> hash
mockPreparedStatement.executeQuery() >> mockResultSet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package gov.hhs.cdc.trustedintermediary.external.hapi
import gov.hhs.cdc.trustedintermediary.DemographicsMock
import gov.hhs.cdc.trustedintermediary.OrderMock
import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext
import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadata
import gov.hhs.cdc.trustedintermediary.etor.orders.OrderConverter
import org.hl7.fhir.r4.model.Address
import org.hl7.fhir.r4.model.ContactPoint
import org.hl7.fhir.r4.model.Extension
import org.hl7.fhir.r4.model.HumanName
import org.hl7.fhir.r4.model.OperationOutcome
import org.hl7.fhir.r4.model.StringType

import java.time.Instant
Expand Down Expand Up @@ -242,6 +244,17 @@ class HapiOrderConverterTest extends Specification {
!contactSection.hasName()
}


def "creating an issue returns a valid OperationOutcomeIssueComponent with Information level severity and code" () {
when:
def output = HapiOrderConverter.getInstance().createInformationIssueComponent("test_details", "test_diagnostics")
then:
output.getSeverity() == OperationOutcome.IssueSeverity.INFORMATION
output.getCode() == OperationOutcome.IssueType.INFORMATIONAL
output.getDetails().getText() == "test_details"
output.getDiagnostics() == "test_diagnostics"
}

Patient fakePatientResource(boolean addHumanName) {

def patient = new Patient()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,24 @@ public DomainResponse constructResponse(int httpStatus, Object objectResponseBod
return response;
}

public DomainResponse constructResponseFromString(int httpStatus, String jsonResponseBody) {
logger.logInfo("Constructing the response");
var response = new DomainResponse(httpStatus);
response.setBody(jsonResponseBody);

response.setHeaders(Map.of(CONTENT_TYPE_LITERAL, APPLICATION_JSON_LITERAL));

return response;
}

public DomainResponse constructOkResponse(Object objectResponseBody) {
return constructResponse(200, objectResponseBody);
}

public DomainResponse constructOkResponseFromString(String jsonBody) {
return constructResponseFromString(200, jsonBody);
}

public DomainResponse constructErrorResponse(int httpStatus, String errorString) {
return constructResponse(httpStatus, Map.of("error", errorString));
}
Expand Down
Loading

0 comments on commit 921f82a

Please sign in to comment.