Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,44 @@
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.util.ClasspathUtil;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.client.interceptor.BasicAuthInterceptor;
import ca.uhn.fhir.validation.FhirValidator;
import ca.uhn.fhir.validation.IValidatorModule;
import gov.cms.madie.madiefhirservice.utils.QiCoreLenientTerminologyValidator;
import gov.cms.madie.madiefhirservice.validators.CustomQiCoreInMemoryValidationSupport;
import gov.cms.madie.madiefhirservice.utils.ResourceUtils;
import gov.cms.madie.madiefhirservice.validators.CustomRemoteTerminologyServiceValidationSupport;
import lombok.extern.slf4j.Slf4j;
import org.hl7.fhir.common.hapi.validation.support.*;
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.ValueSet;
import org.hl7.fhir.r5.context.SimpleWorkerContext;
import org.hl7.fhir.r5.utils.LiquidEngine;
import org.hl7.fhir.utilities.npm.NpmPackage;
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;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.stream.Stream;

@Slf4j
@Configuration
public class HapiFhirConfig {

@Value("${vsac.api-key}")
private String vsacApiKey;

@Value("${vsac.terminology-server-url}")
private String terminologyServerBase;

@Bean
@Qualifier("qicoreFhirContext")
public FhirContext qicoreFhirContext() {
Expand Down Expand Up @@ -81,13 +97,20 @@ public IValidationSupport validationSupportChainQiCore600(
new UnknownCodeSystemWarningValidationSupport(qicore6FhirContext);
unknownCodeSystemWarningValidationSupport.setNonExistentCodeSystemSeverity(
IValidationSupport.IssueSeverity.WARNING);

return new ValidationSupportChain(
npmPackageSupport,
new DefaultProfileValidationSupport(qicore6FhirContext),
new QiCoreLenientTerminologyValidator(qicore6FhirContext),
new CommonCodeSystemsTerminologyService(qicore6FhirContext),
unknownCodeSystemWarningValidationSupport);
PrePopulatedValidationSupport prePopulatedValidationSupport =
getPrePopulatedValidationSupport(qicore6FhirContext);
RemoteTerminologyServiceValidationSupport remoteTerminologyServiceValidationSupport =
getRemoteTerminologyServiceValidationSupport(qicore6FhirContext);

return new CachingValidationSupport(
new ValidationSupportChain(
prePopulatedValidationSupport,
npmPackageSupport,
new DefaultProfileValidationSupport(qicore6FhirContext),
new CustomQiCoreInMemoryValidationSupport(qicore6FhirContext),
new CommonCodeSystemsTerminologyService(qicore6FhirContext),
remoteTerminologyServiceValidationSupport,
unknownCodeSystemWarningValidationSupport));
}

@Bean
Expand Down Expand Up @@ -141,4 +164,32 @@ public String fetchInclude(LiquidEngine liquidEngine, String s) {
return ResourceUtils.getData("/templates/" + s);
}
}

private PrePopulatedValidationSupport getPrePopulatedValidationSupport(
FhirContext qicore6FhirContext) throws IOException {
PrePopulatedValidationSupport prePopulatedValidationSupport =
new PrePopulatedValidationSupport(qicore6FhirContext);
IParser xmlParser = qicore6FhirContext.newXmlParser();
Resource resource = new ClassPathResource("ig_valuesets");
Stream.of(Objects.requireNonNull(resource.getFile().listFiles()))
.forEach(
(file) -> {
String data = ResourceUtils.getData("/ig_valuesets/" + file.getName());
IBaseResource baseResource = xmlParser.parseResource(data);
if (baseResource instanceof ValueSet) {
prePopulatedValidationSupport.addValueSet(baseResource);
}
});
return prePopulatedValidationSupport;
}

public RemoteTerminologyServiceValidationSupport getRemoteTerminologyServiceValidationSupport(
FhirContext qicore6FhirContext) {
CustomRemoteTerminologyServiceValidationSupport remoteTerminologyValidationSupport =
new CustomRemoteTerminologyServiceValidationSupport(
qicore6FhirContext, terminologyServerBase);
remoteTerminologyValidationSupport.addClientInterceptor(
new BasicAuthInterceptor("apikey", vsacApiKey));
return remoteTerminologyValidationSupport;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package gov.cms.madie.madiefhirservice.validators;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.ConceptValidationOptions;
import ca.uhn.fhir.context.support.ValidationSupportContext;
import jakarta.annotation.Nonnull;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService;
import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.ValueSet;

import java.util.Collections;

public class CustomQiCoreInMemoryValidationSupport
extends InMemoryTerminologyServerValidationSupport {
/**
* Constructor
*
* @param theCtx A FhirContext for the FHIR version being validated
*/
public CustomQiCoreInMemoryValidationSupport(FhirContext theCtx) {
super(theCtx);
}

@Override
public CodeValidationResult validateCodeInValueSet(
ValidationSupportContext theValidationSupportContext,
ConceptValidationOptions theOptions,
String theCodeSystemUrlAndVersion,
String theCode,
String theDisplay,
@Nonnull IBaseResource theValueSet) {
ValueSet valueSet = (ValueSet) theValueSet;
if (valueSet.getExpansion() != null
&& CollectionUtils.isNotEmpty(valueSet.getExpansion().getContains())) {
String vsUrl =
CommonCodeSystemsTerminologyService.getValueSetUrl(getFhirContext(), theValueSet);
String codeSystemUrlToValidate = theCodeSystemUrlAndVersion;
String codeSystemVersionToValidate = null;
if (theCodeSystemUrlAndVersion != null) {
int versionIndex = theCodeSystemUrlAndVersion.indexOf("|");
if (versionIndex > -1) {
codeSystemUrlToValidate = theCodeSystemUrlAndVersion.substring(0, versionIndex);
codeSystemVersionToValidate = theCodeSystemUrlAndVersion.substring(versionIndex + 1);
}
}
for (var contains : valueSet.getExpansion().getContains()) {
if (StringUtils.equals(theCode, contains.getCode())) {
IssueSeverity severity = null;
// always assume code system is valid and check for equality
if (theOptions.isInferSystem()
|| (StringUtils.equals(codeSystemUrlToValidate, contains.getSystem())
&& (codeSystemVersionToValidate == null
|| StringUtils.equals(codeSystemVersionToValidate, contains.getVersion())))) {
// check if display matches
if (StringUtils.isNotBlank(theDisplay)
&& !StringUtils.equals(theDisplay, contains.getDisplay())) {
severity = IssueSeverity.ERROR;
}
CodeValidationResult validationResult =
createCodeValidationResult(
theCode,
theDisplay,
codeSystemUrlToValidate,
codeSystemVersionToValidate,
vsUrl,
severity);
if (severity != null) {
String message =
getFhirContext()
.getLocalizer()
.getMessage(
InMemoryTerminologyServerValidationSupport.class,
"displayMismatch",
theDisplay,
contains.getDisplay(),
theCodeSystemUrlAndVersion,
theCode)
+ " for in-memory expansion of ValueSet: "
+ vsUrl;
validationResult.setIssues(
Collections.singletonList(
new CodeValidationIssue(
message,
severity,
CodeValidationIssueCode.INVALID,
CodeValidationIssueCoding.INVALID_DISPLAY)));
if (StringUtils.isNotBlank(message)) {
validationResult.setSourceDetails(message);
}
}
return validationResult;
}
}
}
}
return null;
}

private CodeValidationResult createCodeValidationResult(
String theCode,
String theDisplay,
String theCodeSystem,
String theCodeSystemVersion,
String vsUrl,
IssueSeverity theSeverity) {
CodeValidationResult codeValidationResult =
new CodeValidationResult()
.setCode(theCode)
.setDisplay(theDisplay)
.setCodeSystemName(theCodeSystem)
.setCodeSystemVersion(theCodeSystemVersion)
.setSeverity(theSeverity);
if (StringUtils.isNotBlank(vsUrl)) {
codeValidationResult.setSourceDetails(
"Code was validated against in-memory expansion of ValueSet: " + vsUrl);
}
return codeValidationResult;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package gov.cms.madie.madiefhirservice.validators;

import ca.uhn.fhir.context.FhirContext;
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
import org.hl7.fhir.instance.model.api.IBaseResource;

public class CustomRemoteTerminologyServiceValidationSupport
extends RemoteTerminologyServiceValidationSupport {
public CustomRemoteTerminologyServiceValidationSupport(
FhirContext theFhirContext, String theBaseUrl) {
super(theFhirContext, theBaseUrl);
}

@Override
public IBaseResource fetchCodeSystem(String theSystem) {
// disable code system validation for remote terminology service because it appears that VSAC
// doesn't support code system search with summary mode
// return fetchCodeSystem(theSystem, SummaryEnum.FALSE);
return null;
}

@Override
public String getErrorMessage(String errorCode, Object... theParams) {
return getFhirContext()
.getLocalizer()
.getMessage(getClass().getSuperclass(), errorCode, theParams);
}

@Override
public IBaseResource fetchValueSet(String theValueSetUrl) {
// disable code value set fetch for remote terminology service because it appears that VSAC
// doesn't support value set search with summary mode
// return fetchValueSet(theValueSetUrl, SummaryEnum.FALSE);
return null;
}
}
4 changes: 4 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ okta:
issuer: ${OKTA_ISSUER:https://dev-18092578.okta.com/oauth2/default}
audience: ${OKTA_AUDIENCE:api://default}

vsac:
terminology-server-url: https://cts.nlm.nih.gov/fhir
api-key: ${VSAC_API_KEY:TEST}

caching:
spring:
libraries:
Expand Down
Loading