diff --git a/src/main/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyController.java b/src/main/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyController.java index 974ede9..e7412a2 100644 --- a/src/main/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyController.java +++ b/src/main/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyController.java @@ -3,6 +3,7 @@ import gov.cms.madie.models.measure.ManifestExpansion; import gov.cms.madie.terminology.dto.Code; import gov.cms.madie.terminology.dto.QdmValueSet; +import gov.cms.madie.terminology.dto.ValueSetForSearch; import gov.cms.madie.terminology.dto.ValueSetsSearchCriteria; import gov.cms.madie.terminology.models.CodeSystem; import gov.cms.madie.terminology.models.UmlsUser; @@ -69,6 +70,16 @@ public ResponseEntity> getAllCodeSystems(Principal principal) { return ResponseEntity.ok().body(fhirTerminologyService.getAllCodeSystems()); } + @GetMapping(path = "/search-value-sets", produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + public ResponseEntity> searchValueSets( + Principal principal, @RequestParam Map queryParams) { + final String username = principal.getName(); + UmlsUser umlsUser = vsacService.verifyUmlsAccess(username); + var result = fhirTerminologyService.searchValueSets(umlsUser.getApiKey(), queryParams); + return ResponseEntity.ok().body(result); + } + @GetMapping(path = "/code", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getCode( @RequestParam() String code, diff --git a/src/main/java/gov/cms/madie/terminology/dto/ValueSetForSearch.java b/src/main/java/gov/cms/madie/terminology/dto/ValueSetForSearch.java new file mode 100644 index 0000000..b62f5cb --- /dev/null +++ b/src/main/java/gov/cms/madie/terminology/dto/ValueSetForSearch.java @@ -0,0 +1,25 @@ +package gov.cms.madie.terminology.dto; + +import lombok.Builder; +import lombok.Data; +import org.hl7.fhir.r4.model.Enumerations; + +@Data +@Builder +public class ValueSetForSearch { + private String title; + private String name; + private String author; + private String composedOf; + private String effectiveDate; + private String lastReviewDate; + private String lastUpdated; + private String publisher; + private String purpose; + private String url; + private String oid; + private String steward; + private String version; + private String codeSystem; + private Enumerations.PublicationStatus status; +} diff --git a/src/main/java/gov/cms/madie/terminology/service/FhirTerminologyService.java b/src/main/java/gov/cms/madie/terminology/service/FhirTerminologyService.java index 520e7f3..9064b04 100644 --- a/src/main/java/gov/cms/madie/terminology/service/FhirTerminologyService.java +++ b/src/main/java/gov/cms/madie/terminology/service/FhirTerminologyService.java @@ -4,10 +4,7 @@ import ca.uhn.fhir.parser.IParser; import gov.cms.madie.models.mapping.CodeSystemEntry; import gov.cms.madie.models.measure.ManifestExpansion; -import gov.cms.madie.terminology.dto.Code; -import gov.cms.madie.terminology.dto.CodeStatus; -import gov.cms.madie.terminology.dto.QdmValueSet; -import gov.cms.madie.terminology.dto.ValueSetsSearchCriteria; +import gov.cms.madie.terminology.dto.*; import gov.cms.madie.terminology.models.CodeSystem; import gov.cms.madie.terminology.models.UmlsUser; import gov.cms.madie.terminology.repositories.CodeSystemRepository; @@ -16,16 +13,17 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Parameters; -import org.hl7.fhir.r4.model.ValueSet; +import org.hl7.fhir.r4.model.*; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.web.util.UriComponentsBuilder; import java.time.Instant; -import java.util.*; import java.util.stream.Collectors; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; @Service @Slf4j @@ -120,6 +118,49 @@ private List getValueSetConcepts( return List.of(); } + public List searchValueSets(String apiKey, Map queryParams) { + IParser parser = fhirContext.newJsonParser(); + String responseString = fhirTerminologyServiceWebClient.searchValueSets(apiKey, queryParams); + Bundle bundle = parser.parseResource(Bundle.class, responseString); + List valueSetList = new ArrayList<>(); + bundle + .getEntry() + .forEach( + entry -> { + Resource resource = entry.getResource(); + ValueSet vs = (ValueSet) resource; + if (resource instanceof ValueSet) { + String oid = ""; + for (Identifier identifier : ((ValueSet) resource).getIdentifier()) { + if (identifier.getValue() != null && !identifier.getValue().isEmpty()) { + oid = identifier.getValue(); + } + } + ValueSetForSearch valueSet = + ValueSetForSearch.builder() + .title(vs.getTitle()) + .author(String.valueOf(vs.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/valueset-author").getValue())) + .name(vs.getName()) + .composedOf(vs.getCompose().getInclude().stream().map(x -> x.getSystem()).collect(Collectors.joining(","))) + .effectiveDate(String.valueOf(vs.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/valueset-effectiveDate").getValue())) + .lastReviewDate(String.valueOf(vs.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/resource-lastReviewDate").getValue())) + .lastUpdated(vs.getMeta().getLastUpdated().toString()) + .url(vs.getUrl()) + .version(vs.getVersion()) + .status(vs.getStatus()) + .publisher(vs.getPublisher()) + .purpose(vs.getPurpose()) + .steward(vs.getPublisher()) + .oid(oid) + .build(); + valueSetList.add(valueSet); + } + log.info("valueSetList {}", valueSetList); + }); + + return valueSetList; + } + public List getAllCodeSystems() { // remove items that are marked as not present in vsac to cut expense List codeSystemMappingEntries = @@ -211,7 +252,7 @@ private void recursiveRetrieveCodeSystems( entry -> { var codeSystem = (org.hl7.fhir.r4.model.CodeSystem) entry.getResource(); String codeSystemValue = ""; - for (org.hl7.fhir.r4.model.Identifier identifier : codeSystem.getIdentifier()) { + for (Identifier identifier : codeSystem.getIdentifier()) { if (identifier.getValue() != null && !identifier.getValue().isEmpty()) { codeSystemValue = identifier.getValue(); break; diff --git a/src/main/java/gov/cms/madie/terminology/webclient/FhirTerminologyServiceWebClient.java b/src/main/java/gov/cms/madie/terminology/webclient/FhirTerminologyServiceWebClient.java index dba948a..8d6d29b 100644 --- a/src/main/java/gov/cms/madie/terminology/webclient/FhirTerminologyServiceWebClient.java +++ b/src/main/java/gov/cms/madie/terminology/webclient/FhirTerminologyServiceWebClient.java @@ -11,6 +11,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; @@ -28,13 +30,15 @@ public class FhirTerminologyServiceWebClient { private final String codeSystemPath; private final String codeLookupsUrl; private final String defaultProfile; + private final String searchValueSetEndpoint; public FhirTerminologyServiceWebClient( @Value("${client.fhir-terminology-service.base-url}") String fhirTerminologyServiceBaseUrl, @Value("${client.fhir-terminology-service.manifests-urn}") String manifestUrn, @Value("${client.fhir-terminology-service.code-system-urn}") String codeSystemUrn, @Value("${client.fhir-terminology-service.code-lookups}") String codeLookupsUrl, - @Value("${client.default_profile}") String defaultProfile) { + @Value("${client.default_profile}") String defaultProfile, + @Value("${client.search_value_set_endpoint}") String searchValueSetEndpoint) { fhirTerminologyWebClient = WebClient.builder() .baseUrl(fhirTerminologyServiceBaseUrl) @@ -46,6 +50,7 @@ public FhirTerminologyServiceWebClient( this.codeSystemPath = codeSystemUrn; this.codeLookupsUrl = codeLookupsUrl; this.defaultProfile = defaultProfile; + this.searchValueSetEndpoint = searchValueSetEndpoint; } public String getManifestBundle(String apiKey) { @@ -59,6 +64,27 @@ public String getCodeSystemsPage(Integer offset, Integer count, String apiKey) { return fetchResourceFromVsac(codeUri.toString(), apiKey, "CodeSystem"); } + public String searchValueSets(String apiKey, Map queryParams) { + if (queryParams.containsKey("url")) { + String urlValue = queryParams.get("url"); + // if user didnt add htpp:// we do + if (!urlValue.startsWith("http://")) { + urlValue = "http://" + urlValue; + } + queryParams.put("url", urlValue); + } + MultiValueMap multiValueMap = new LinkedMultiValueMap<>(); + multiValueMap.setAll(queryParams); + URI uri = + UriComponentsBuilder.fromUriString(searchValueSetEndpoint) + .queryParams(multiValueMap) + .encode() + .build() + .toUri(); + log.info("value set search url is: {}", uri.toString()); + return fetchResourceFromVsac(uri.toString(), apiKey, "bundle"); + } + public String getValueSetResource( String apiKey, ValueSetsSearchCriteria.ValueSetParams valueSetParams, diff --git a/src/main/java/gov/cms/madie/terminology/webclient/TerminologyServiceWebClient.java b/src/main/java/gov/cms/madie/terminology/webclient/TerminologyServiceWebClient.java index 5a8dd1a..352b4c9 100644 --- a/src/main/java/gov/cms/madie/terminology/webclient/TerminologyServiceWebClient.java +++ b/src/main/java/gov/cms/madie/terminology/webclient/TerminologyServiceWebClient.java @@ -29,7 +29,6 @@ public TerminologyServiceWebClient( @Value("${client.vsac_base_url}") String baseUrl, @Value("${client.valueset_endpoint}") String valueSetEndpoint, @Value("${client.default_profile}") String defaultProfile) { - this.terminologyClient = webClientBuilder.baseUrl(baseUrl).build(); this.baseUrl = baseUrl; this.valueSetEndpoint = valueSetEndpoint; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c5a15ff..6767adb 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -24,6 +24,7 @@ code-system-refresh-task: code-system-cron-date-time: ${CS_REFRESH_TIME:@midnight} client: + search_value_set_endpoint: ${SEARCH_VALUE_SET_ENDPOINT:https://cts.nlm.nih.gov/fhir/ValueSet} vsac_base_url: https://vsac.nlm.nih.gov/vsac valueset_endpoint: /svs/RetrieveMultipleValueSets?id={oid}&profile={profile}&includeDraft={includeDraft} default_profile: ${DEFAULT_VSAC_PROFILE:Most Recent Code System Versions in VSAC} diff --git a/src/test/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyControllerTest.java b/src/test/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyControllerTest.java index 2ae6f6f..a576d46 100644 --- a/src/test/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyControllerTest.java +++ b/src/test/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyControllerTest.java @@ -1,10 +1,7 @@ package gov.cms.madie.terminology.controller; import gov.cms.madie.models.measure.ManifestExpansion; -import gov.cms.madie.terminology.dto.Code; -import gov.cms.madie.terminology.dto.CodeStatus; -import gov.cms.madie.terminology.dto.QdmValueSet; -import gov.cms.madie.terminology.dto.ValueSetsSearchCriteria; +import gov.cms.madie.terminology.dto.*; import gov.cms.madie.terminology.exceptions.VsacUnauthorizedException; import gov.cms.madie.terminology.models.CodeSystem; import gov.cms.madie.terminology.models.UmlsUser; @@ -257,4 +254,25 @@ void testGetCodesList() { assertEquals(response.getStatusCode(), HttpStatus.OK); assertEquals(response.getBody().get(0), code); } + + @Test + void testSearchValueSets() { + List mockValueSets = new ArrayList<>(); + ValueSetForSearch v1 = ValueSetForSearch.builder().title("title 1").name("title1").url("url").oid("oid") + .steward("steward").version("version").codeSystem("cs").build(); + ValueSetForSearch v2 = ValueSetForSearch.builder().title("title 2").name("title2").url("url").oid("oid") + .steward("steward").version("version").codeSystem("cs").build(); + mockValueSets.add(v1); + mockValueSets.add(v2); + Principal principal = mock(Principal.class); + when(principal.getName()).thenReturn(TEST_USER); + when(vsacService.verifyUmlsAccess(anyString())).thenReturn(umlsUser); + when(fhirTerminologyService.searchValueSets(any(), any())).thenReturn(mockValueSets); + Map queryParams = new HashMap<>(); + queryParams.put("param1", "value1"); + queryParams.put("param2", "value2"); + ResponseEntity> response = + vsacFhirTerminologyController.searchValueSets(principal, queryParams); + assertEquals(response.getStatusCode(), HttpStatus.OK); + } } diff --git a/src/test/java/gov/cms/madie/terminology/webclient/FhirTerminologyServiceWebClientTest.java b/src/test/java/gov/cms/madie/terminology/webclient/FhirTerminologyServiceWebClientTest.java index 8fb177d..14b6606 100644 --- a/src/test/java/gov/cms/madie/terminology/webclient/FhirTerminologyServiceWebClientTest.java +++ b/src/test/java/gov/cms/madie/terminology/webclient/FhirTerminologyServiceWebClientTest.java @@ -27,6 +27,7 @@ class FhirTerminologyServiceWebClientTest { private static final String MOCK_CODE_LOOKUP = "/CodeSystem/$lookup"; private static final String DEFAULT_PROFILE = "Most Recent Code System Versions in VSAC"; public static MockWebServer mockBackEnd; + private static final String SEARCH_VALUE_SET_ENDPOINT = "https://cts.nlm.nih.gov/fhir/ValueSet"; private ValueSetsSearchCriteria.ValueSetParams testValueSetParams; @@ -44,7 +45,12 @@ void initialize() { String baseUrl = String.format("http://localhost:%s", mockBackEnd.getPort()); fhirTerminologyServiceWebClient = new FhirTerminologyServiceWebClient( - baseUrl, MOCK_MANIFEST_URN, MOCK_CODE_SYSTEM_URN, MOCK_CODE_LOOKUP, DEFAULT_PROFILE); + baseUrl, + MOCK_MANIFEST_URN, + MOCK_CODE_SYSTEM_URN, + MOCK_CODE_LOOKUP, + DEFAULT_PROFILE, + SEARCH_VALUE_SET_ENDPOINT); } @AfterAll