Skip to content

Commit

Permalink
Merge pull request #106 from MeasureAuthoringTool/MAT-7855
Browse files Browse the repository at this point in the history
MAT-7855 Integrate HAPI FHIR Server into Test Case Builder for internal value set expansion
  • Loading branch information
adongare authored Dec 11, 2024
2 parents 31836ff + 97cdb53 commit d6c5177
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 14 deletions.
9 changes: 8 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<name>Terminology Service</name>
<description>Terminology Service for MADiE</description>
<properties>
<hapifhir.version>7.6.0</hapifhir.version>
<java.version>16</java.version>
<jxbmavenplugin.version>2.5.0</jxbmavenplugin.version>
<maven.compiler.source>17</maven.compiler.source>
Expand Down Expand Up @@ -81,7 +82,13 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-r4</artifactId>
<version>7.6.0</version>
<version>${hapifhir.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/ca.uhn.hapi.fhir/hapi-fhir-client -->
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-client</artifactId>
<version>${hapifhir.version}</version>
</dependency>

<dependency>
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/gov/cms/madie/terminology/config/HapiFhirConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package gov.cms.madie.terminology.config;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class HapiFhirConfig {
@Value("${hapi-fhir-url}")
private String hapiFhirUrl;

@Bean
@Qualifier("hapiClient")
public IGenericClient createHapiClient(@Autowired FhirContext fhirContext) {
return fhirContext.newRestfulGenericClient(hapiFhirUrl);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package gov.cms.madie.terminology.controller;

import ca.uhn.fhir.context.FhirContext;
import gov.cms.madie.terminology.service.InternalTerminologyService;
import lombok.RequiredArgsConstructor;
import org.hl7.fhir.r4.model.ValueSet;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(path = "/internal-terminology")
@RequiredArgsConstructor
public class InternalTerminologyController {

private final FhirContext fhirContext;
private final InternalTerminologyService internalTerminologyService;

@GetMapping(path = "ValueSet/{id}/expand", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> valueSetExpansion(@PathVariable String id) {
ValueSet valueSet = internalTerminologyService.getValueSetExpansionById(id);
return ResponseEntity.ok().body(fhirContext.newJsonParser().encodeResourceToString(valueSet));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.reactive.function.client.WebClientResponseException;

import gov.cms.madie.terminology.exceptions.VsacGenericException;
import gov.cms.madie.terminology.exceptions.HapiOperationException;
import gov.cms.madie.terminology.exceptions.VsacResourceNotFoundException;
import gov.cms.madie.terminology.exceptions.VsacUnauthorizedException;

Expand Down Expand Up @@ -107,14 +107,14 @@ Map<String, Object> onVsacResourceNotFoundException(
return errorAttributes1;
}

@ExceptionHandler(VsacGenericException.class)
@ExceptionHandler(HapiOperationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
Map<String, Object> onVsacGenericException(VsacGenericException ex, WebRequest request) {
Map<String, String> validationErrors = new HashMap<>();
validationErrors.put(request.getContextPath(), ex.getMessage());
Map<String, Object> onHapiOperationException(HapiOperationException ex, WebRequest request) {
Map<String, String> errors = new HashMap<>();
errors.put(request.getContextPath(), ex.getMessage());
Map<String, Object> errorAttributes = getErrorAttributes(request, HttpStatus.BAD_REQUEST);
errorAttributes.put("validationErrors", validationErrors);
errorAttributes.put("errors", errors);
return errorAttributes;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package gov.cms.madie.terminology.exceptions;

public class HapiOperationException extends RuntimeException {
public HapiOperationException(String message) {
super(message);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package gov.cms.madie.terminology.service;

import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.server.exceptions.*;
import gov.cms.madie.terminology.exceptions.HapiOperationException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.ValueSet;
import org.springframework.stereotype.Service;

import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class InternalTerminologyService {
private final IGenericClient hapiClient;

/**
* This method fetches the ValueSet expansion by id from HAPI server
*
* @param id -> Value Set id
* @return ValueSet -> Value Set with expansion details
*/
public ValueSet getValueSetExpansionById(String id) {
try {
log.info("Fetching ValueSet expansion for {}", id);
Parameters parameters =
hapiClient
.operation()
.onInstance(new IdType("ValueSet", id))
.named("$expand")
.withNoParameters(Parameters.class)
.execute();
return (ValueSet) parameters.getParameter().get(0).getResource();
} catch (BaseServerResponseException ex) {
log.error("An error occurred while fetching expansion for the ValueSet[{}]", id, ex);
OperationOutcome outcome = (OperationOutcome) ex.getOperationOutcome();
if (outcome != null) {
String errors =
outcome.getIssue().stream()
.map(OperationOutcome.OperationOutcomeIssueComponent::getDiagnostics)
.collect(Collectors.joining("\n"));
throw new HapiOperationException(errors);
} else {
throw new HapiOperationException(ex.getMessage());
}
}
}
}
2 changes: 2 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ client:
code-system-urn: /CodeSystem
code-lookups: /CodeSystem/$lookup?system={fullUrl}&code={code}&version={version}

hapi-fhir-url: ${HAPI_FHIR_URL:http://localhost:8888/fhir}

spring:
session:
store-type: none
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package gov.cms.madie.terminology.controller;

import ca.uhn.fhir.context.FhirContext;
import gov.cms.madie.terminology.exceptions.HapiOperationException;
import gov.cms.madie.terminology.service.InternalTerminologyService;
import org.hl7.fhir.r4.model.ValueSet;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import java.security.Principal;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent;
import static org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionComponent;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(InternalTerminologyController.class)
class InternalTerminologyControllerMvcTest {

@MockBean private InternalTerminologyService internalTerminologyService;
@MockBean private FhirContext fhirContext;

@Autowired private MockMvc mockMvc;
private static final String TEST_USER = "test.user";

@Test
void testGetValueSetExpansion() throws Exception {
Principal principal = mock(Principal.class);
when(principal.getName()).thenReturn(TEST_USER);

var contains = new ValueSetExpansionContainsComponent();
contains.setCode("02").setDisplay("trivalent poliovirus vaccine, live, oral").setSystem("CVX");
var expansion = new ValueSetExpansionComponent();
expansion.addContains(contains);
ValueSet valueSet = new ValueSet();
valueSet.setId("us-core-vaccines-cvx-1");
valueSet.setExpansion(expansion);

when(internalTerminologyService.getValueSetExpansionById(anyString())).thenReturn(valueSet);
when(fhirContext.newJsonParser()).thenReturn(FhirContext.forR4().newJsonParser());
MvcResult result =
mockMvc
.perform(
MockMvcRequestBuilders.get(
"/internal-terminology/ValueSet/us-core-vaccines-cvx-1/expand")
.with(user(TEST_USER))
.with(csrf())
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isOk())
.andReturn();
assertThat(result.getResponse().getStatus(), is(equalTo(200)));
String content = result.getResponse().getContentAsString();
verify(internalTerminologyService, times(1)).getValueSetExpansionById(anyString());
assertThat(
content,
containsString(
"{\"resourceType\":\"ValueSet\",\"id\":\"us-core-vaccines-cvx-1\",\"expansion\":{\"contains\":[{\"system\":\"CVX\",\"code\":\"02\",\"display\":\"trivalent poliovirus vaccine, live, oral\"}]}}"));
}

@Test
void testGetValueSetExpansionIfNotFound() throws Exception {
Principal principal = mock(Principal.class);
when(principal.getName()).thenReturn(TEST_USER);

doThrow(new HapiOperationException("Value set not found"))
.when(internalTerminologyService)
.getValueSetExpansionById(anyString());
MvcResult result =
mockMvc
.perform(
MockMvcRequestBuilders.get(
"/internal-terminology/ValueSet/us-core-vaccines-cvx-1/expand")
.with(user(TEST_USER))
.with(csrf())
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isBadRequest())
.andReturn();
String content = result.getResponse().getContentAsString();
verify(internalTerminologyService, times(1)).getValueSetExpansionById(anyString());
assertThat(content, containsString("Value set not found"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package gov.cms.madie.terminology.service;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.gclient.*;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import gov.cms.madie.terminology.exceptions.HapiOperationException;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.ValueSet;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;

import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
public class InternalTerminologyServiceTest {

@Mock private IGenericClient hapiClient;

@InjectMocks private InternalTerminologyService service;

private IOperation operation;
private IOperationUnnamed operationUnnamed;
private IOperationUntyped operationUntyped;
private IOperationUntypedWithInput operationUntypedWithInput;

@BeforeEach
public void setUp() {
operation = mock(IOperation.class);
operationUnnamed = mock(IOperationUnnamed.class);
operationUntyped = mock(IOperationUntyped.class);
operationUntypedWithInput =
(IOperationUntypedWithInput<Parameters>) mock(IOperationUntypedWithInput.class);
}

@Test
void testGetValueSetExpansionById() {
String valueSetId = "us-core-vaccines-cvx-1";
var idType = new IdType("ValueSet", valueSetId);

// mock hapi client operation
when(hapiClient.operation()).thenReturn(operation);
when(operation.onInstance(idType)).thenReturn(operationUnnamed);
when(operationUnnamed.named("$expand")).thenReturn(operationUntyped);
when(operationUntyped.withNoParameters(Parameters.class)).thenReturn(operationUntypedWithInput);

// mock response
ValueSet valueSet = new ValueSet();
Parameters parameters = new Parameters();
parameters.addParameter().setResource(valueSet);
when(operationUntypedWithInput.execute()).thenReturn(parameters);

ValueSet expansion = service.getValueSetExpansionById(valueSetId);
assertThat(expansion, is(not(nullValue())));
}

@Test
void testGetValueSetExpansionByIdIfValueSetNotFound() {
String valueSetId = "us-core-vaccines-cvx-1";
var idType = new IdType("ValueSet", valueSetId);
FhirContext fhirContext = FhirContext.forR4();
// mock hapi client operation
when(hapiClient.operation()).thenReturn(operation);
when(operation.onInstance(idType)).thenReturn(operationUnnamed);
when(operationUnnamed.named("$expand")).thenReturn(operationUntyped);
when(operationUntyped.withNoParameters(Parameters.class)).thenReturn(operationUntypedWithInput);

// mock response
IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.newInstance(fhirContext);
OperationOutcomeUtil.addIssue(
fhirContext, operationOutcome, "warning", "Resource not found", null, null);
ResourceNotFoundException resourceNotFoundException = new ResourceNotFoundException(idType);
resourceNotFoundException.setOperationOutcome(operationOutcome);
doThrow(resourceNotFoundException).when(operationUntypedWithInput).execute();

Exception ex =
Assertions.assertThrows(
HapiOperationException.class, () -> service.getValueSetExpansionById(valueSetId));
assertThat(ex.getMessage(), is(equalTo("Resource not found")));
}
}

0 comments on commit d6c5177

Please sign in to comment.