Skip to content

Commit 77a1eba

Browse files
authored
Merge branch 'main' into NIAD-3234-fhir-parser-service-refactor
2 parents 6db26ce + bde8dcc commit 77a1eba

File tree

4 files changed

+429
-20
lines changed

4 files changed

+429
-20
lines changed

service/src/main/java/uk/nhs/adaptors/gp2gp/common/service/FhirParseService.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import org.hl7.fhir.instance.model.api.IBaseResource;
44
import org.springframework.stereotype.Service;
5-
65
import ca.uhn.fhir.context.FhirContext;
76
import ca.uhn.fhir.parser.IParser;
87
import ca.uhn.fhir.parser.StrictErrorHandler;
@@ -30,4 +29,8 @@ private IParser prepareParser(FhirContext fhirContext) {
3029
fhirContext.setParserErrorHandler(new StrictErrorHandler());
3130
return fhirContext.newJsonParser();
3231
}
32+
33+
public String encodeToJson(IBaseResource resource) {
34+
return jsonParser.setPrettyPrint(true).encodeResourceToString(resource);
35+
}
3336
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package uk.nhs.adaptors.gp2gp.ehr;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.hl7.fhir.dstu3.model.CodeableConcept;
6+
import org.hl7.fhir.dstu3.model.Coding;
7+
import org.hl7.fhir.dstu3.model.Meta;
8+
import org.hl7.fhir.dstu3.model.OperationOutcome;
9+
import org.hl7.fhir.dstu3.model.UriType;
10+
import org.springframework.beans.factory.annotation.Autowired;
11+
import org.springframework.http.HttpStatus;
12+
import org.springframework.http.ResponseEntity;
13+
import org.springframework.web.bind.annotation.PathVariable;
14+
import org.springframework.web.bind.annotation.PostMapping;
15+
import org.springframework.web.bind.annotation.RequestMapping;
16+
import org.springframework.web.bind.annotation.RestController;
17+
import uk.nhs.adaptors.gp2gp.common.service.FhirParseService;
18+
import uk.nhs.adaptors.gp2gp.common.service.RandomIdGeneratorService;
19+
import uk.nhs.adaptors.gp2gp.common.service.TimestampService;
20+
import uk.nhs.adaptors.gp2gp.common.task.TaskDispatcher;
21+
import uk.nhs.adaptors.gp2gp.ehr.model.EhrExtractStatus;
22+
import uk.nhs.adaptors.gp2gp.gpc.GetGpcStructuredTaskDefinition;
23+
24+
import java.util.Collections;
25+
26+
@Slf4j
27+
@RestController
28+
@AllArgsConstructor(onConstructor = @__(@Autowired))
29+
@RequestMapping(path = "/ehr-resend")
30+
public class EhrResendController {
31+
32+
private static final String OPERATION_OUTCOME_URL = "https://fhir.nhs.uk/STU3/StructureDefinition/GPConnect-OperationOutcome-1";
33+
private static final String PRECONDITION_FAILED = "PRECONDITION_FAILED";
34+
private static final String INVALID_IDENTIFIER_VALUE = "INVALID_IDENTIFIER_VALUE";
35+
36+
private EhrExtractStatusRepository ehrExtractStatusRepository;
37+
private TaskDispatcher taskDispatcher;
38+
private RandomIdGeneratorService randomIdGeneratorService;
39+
private final TimestampService timestampService;
40+
private final FhirParseService fhirParseService;
41+
42+
@PostMapping("/{conversationId}")
43+
public ResponseEntity<String> scheduleEhrExtractResend(@PathVariable String conversationId) {
44+
EhrExtractStatus ehrExtractStatus = ehrExtractStatusRepository.findByConversationId(conversationId).orElseGet(() -> null);
45+
46+
if (ehrExtractStatus == null) {
47+
var details = getCodeableConcept(INVALID_IDENTIFIER_VALUE);
48+
var diagnostics = "Provide a conversationId that exists and retry the operation";
49+
50+
var operationOutcome = createOperationOutcome(OperationOutcome.IssueType.VALUE,
51+
OperationOutcome.IssueSeverity.ERROR,
52+
details,
53+
diagnostics);
54+
var errorBody = fhirParseService.encodeToJson(operationOutcome);
55+
56+
return new ResponseEntity<>(errorBody, HttpStatus.NOT_FOUND);
57+
}
58+
59+
if (hasNoErrorsInEhrReceivedAcknowledgement(ehrExtractStatus) && ehrExtractStatus.getError() == null) {
60+
61+
var details = getCodeableConcept(PRECONDITION_FAILED);
62+
var diagnostics = "The current resend operation is still in progress. Please wait for it to complete before retrying";
63+
var operationOutcome = createOperationOutcome(OperationOutcome.IssueType.BUSINESSRULE,
64+
OperationOutcome.IssueSeverity.ERROR,
65+
details,
66+
diagnostics);
67+
var errorBody = fhirParseService.encodeToJson(operationOutcome);
68+
return new ResponseEntity<>(errorBody, HttpStatus.CONFLICT);
69+
}
70+
71+
var updatedEhrExtractStatus = prepareEhrExtractStatusForNewResend(ehrExtractStatus);
72+
ehrExtractStatusRepository.save(updatedEhrExtractStatus);
73+
createGetGpcStructuredTask(updatedEhrExtractStatus);
74+
LOGGER.info("Scheduled GetGpcStructuredTask for resend of ConversationId: {}", conversationId);
75+
76+
return new ResponseEntity<>(HttpStatus.ACCEPTED);
77+
}
78+
79+
private static CodeableConcept getCodeableConcept(String codeableConceptCode) {
80+
return new CodeableConcept().addCoding(
81+
new Coding("https://fhir.nhs.uk/STU3/ValueSet/Spine-ErrorOrWarningCode-1", codeableConceptCode, null));
82+
}
83+
84+
private static boolean hasNoErrorsInEhrReceivedAcknowledgement(EhrExtractStatus ehrExtractStatus) {
85+
var ehrReceivedAcknowledgement = ehrExtractStatus.getEhrReceivedAcknowledgement();
86+
if (ehrReceivedAcknowledgement == null) {
87+
return true;
88+
}
89+
90+
var errors = ehrReceivedAcknowledgement.getErrors();
91+
if (errors == null || errors.isEmpty()) {
92+
return true;
93+
}
94+
return false;
95+
}
96+
97+
private EhrExtractStatus prepareEhrExtractStatusForNewResend(EhrExtractStatus ehrExtractStatus) {
98+
99+
var now = timestampService.now();
100+
ehrExtractStatus.setUpdatedAt(now);
101+
ehrExtractStatus.setMessageTimestamp(now);
102+
ehrExtractStatus.setEhrExtractCorePending(null);
103+
ehrExtractStatus.setGpcAccessDocument(null);
104+
ehrExtractStatus.setEhrContinue(null);
105+
ehrExtractStatus.setEhrReceivedAcknowledgement(null);
106+
107+
return ehrExtractStatus;
108+
}
109+
110+
private void createGetGpcStructuredTask(EhrExtractStatus ehrExtractStatus) {
111+
var getGpcStructuredTaskDefinition = GetGpcStructuredTaskDefinition.getGetGpcStructuredTaskDefinition(randomIdGeneratorService,
112+
ehrExtractStatus);
113+
taskDispatcher.createTask(getGpcStructuredTaskDefinition);
114+
}
115+
116+
public static OperationOutcome createOperationOutcome(
117+
OperationOutcome.IssueType type, OperationOutcome.IssueSeverity severity, CodeableConcept details, String diagnostics) {
118+
var operationOutcome = new OperationOutcome();
119+
Meta meta = new Meta();
120+
meta.setProfile(Collections.singletonList(new UriType(OPERATION_OUTCOME_URL)));
121+
operationOutcome.setMeta(meta);
122+
operationOutcome.addIssue()
123+
.setCode(type)
124+
.setSeverity(severity)
125+
.setDetails(details)
126+
.setDiagnostics(diagnostics);
127+
return operationOutcome;
128+
}
129+
130+
}
Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,65 @@
11
package uk.nhs.adaptors.gp2gp.common.service;
22

3-
import ca.uhn.fhir.context.FhirContext;
4-
import ca.uhn.fhir.parser.IParser;
5-
import ca.uhn.fhir.parser.StrictErrorHandler;
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.JsonNode;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import org.hl7.fhir.dstu3.model.CodeableConcept;
7+
import org.hl7.fhir.dstu3.model.Coding;
8+
import org.hl7.fhir.dstu3.model.OperationOutcome;
9+
import org.junit.jupiter.api.BeforeEach;
610
import org.junit.jupiter.api.Test;
7-
import org.mockito.ArgumentCaptor;
11+
import org.junit.jupiter.api.extension.ExtendWith;
12+
import org.mockito.junit.jupiter.MockitoExtension;
13+
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
14+
import uk.nhs.adaptors.gp2gp.common.configuration.ObjectMapperBean;
15+
import uk.nhs.adaptors.gp2gp.ehr.EhrResendController;
16+
import java.util.List;
17+
import static org.junit.jupiter.api.Assertions.assertEquals;
818

9-
import static org.junit.jupiter.api.Assertions.assertNotNull;
10-
import static org.mockito.Mockito.mock;
11-
import static org.mockito.Mockito.times;
12-
import static org.mockito.Mockito.verify;
13-
import static org.mockito.Mockito.when;
1419

20+
@ExtendWith(MockitoExtension.class)
1521
class FhirParseServiceTest {
1622

17-
private ArgumentCaptor<StrictErrorHandler> captor = ArgumentCaptor.forClass(StrictErrorHandler.class);
23+
public static final String INVALID_IDENTIFIER_VALUE = "INVALID_IDENTIFIER_VALUE";
24+
private static final String OPERATION_OUTCOME_URL = "https://fhir.nhs.uk/STU3/StructureDefinition/GPConnect-OperationOutcome-1";
25+
private OperationOutcome operationOutcome;
26+
private ObjectMapper objectMapper;
27+
28+
@BeforeEach
29+
void setUp() {
30+
ObjectMapperBean objectMapperBean = new ObjectMapperBean();
31+
objectMapper = objectMapperBean.objectMapper(new Jackson2ObjectMapperBuilder());
32+
33+
var details = getCodeableConcept();
34+
var diagnostics = "Provide a conversationId that exists and retry the operation";
35+
operationOutcome = EhrResendController.createOperationOutcome(OperationOutcome.IssueType.VALUE,
36+
OperationOutcome.IssueSeverity.ERROR,
37+
details,
38+
diagnostics);
39+
}
1840

1941
@Test
20-
void fhirParseServiceInitializedWithStrictErrorHandlerAndNewJsonParserTest() {
42+
void ableToEncodeOperationOutcomeToJson() throws JsonProcessingException {
43+
FhirParseService fhirParseService = new FhirParseService();
2144

22-
FhirContext fhirContext = mock(FhirContext.class);
23-
IParser parser = mock(IParser.class);
24-
when(fhirContext.newJsonParser()).thenReturn(parser);
45+
String convertedToJsonOperationOutcome = fhirParseService.encodeToJson(operationOutcome);
2546

26-
FhirParseService service = new FhirParseService(fhirContext);
47+
JsonNode rootNode = objectMapper.readTree(convertedToJsonOperationOutcome);
48+
String code =
49+
rootNode.path("issue").get(0).path("details").path("coding").get(0).path("code").asText();
50+
String operationOutcomeUrl = rootNode.path("meta").path("profile").get(0).asText();
51+
52+
assertEquals(INVALID_IDENTIFIER_VALUE, code);
53+
assertEquals(OPERATION_OUTCOME_URL, operationOutcomeUrl);
54+
}
2755

28-
verify(fhirContext).setParserErrorHandler(captor.capture());
29-
StrictErrorHandler strictErrorHandler = captor.getValue();
30-
assertNotNull(strictErrorHandler, "StrictErrorHandler should not be null");
31-
verify(fhirContext, times(2)).newJsonParser();
56+
private static CodeableConcept getCodeableConcept() {
57+
var details = new CodeableConcept();
58+
var codeableConceptCoding = new Coding();
59+
codeableConceptCoding.setSystem("http://fhir.nhs.net/ValueSet/gpconnect-error-or-warning-code-1");
60+
codeableConceptCoding.setCode(FhirParseServiceTest.INVALID_IDENTIFIER_VALUE);
61+
details.setCoding(List.of(codeableConceptCoding));
62+
return details;
3263
}
64+
3365
}

0 commit comments

Comments
 (0)