001package org.hl7.fhir.common.hapi.validation.support; 002 003import ca.uhn.fhir.context.FhirContext; 004import ca.uhn.fhir.context.support.ConceptValidationOptions; 005import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; 006import ca.uhn.fhir.context.support.IValidationSupport; 007import ca.uhn.fhir.context.support.ValidationSupportContext; 008import ca.uhn.fhir.rest.client.api.IGenericClient; 009import ca.uhn.fhir.util.BundleUtil; 010import ca.uhn.fhir.util.ParametersUtil; 011import org.apache.commons.lang3.Validate; 012import org.hl7.fhir.instance.model.api.IBaseBundle; 013import org.hl7.fhir.instance.model.api.IBaseParameters; 014import org.hl7.fhir.instance.model.api.IBaseResource; 015import org.hl7.fhir.r4.model.CodeSystem; 016 017import javax.annotation.Nonnull; 018import java.util.ArrayList; 019import java.util.List; 020 021import static org.apache.commons.lang3.StringUtils.isBlank; 022import static org.apache.commons.lang3.StringUtils.isNotBlank; 023 024/** 025 * This class is an implementation of {@link IValidationSupport} that fetches validation codes 026 * from a remote FHIR based terminology server. It will invoke the FHIR 027 * <a href="http://hl7.org/fhir/valueset-operation-validate-code.html">ValueSet/$validate-code</a> 028 * operation in order to validate codes. 029 */ 030public class RemoteTerminologyServiceValidationSupport extends BaseValidationSupport implements IValidationSupport { 031 032 private String myBaseUrl; 033 private List<Object> myClientInterceptors = new ArrayList<>(); 034 035 /** 036 * Constructor 037 * 038 * @param theFhirContext The FhirContext object to use 039 */ 040 public RemoteTerminologyServiceValidationSupport(FhirContext theFhirContext) { 041 super(theFhirContext); 042 } 043 044 @Override 045 public CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) { 046 return invokeRemoteValidateCode(theCodeSystem, theCode, theDisplay, theValueSetUrl, null); 047 } 048 049 @Override 050 public CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) { 051 IBaseResource valueSet = theValueSet; 052 String valueSetUrl = DefaultProfileValidationSupport.getConformanceResourceUrl(myCtx, valueSet); 053 if (isNotBlank(valueSetUrl)) { 054 valueSet = null; 055 } else { 056 valueSetUrl = null; 057 } 058 return invokeRemoteValidateCode(theCodeSystem, theCode, theDisplay, valueSetUrl, valueSet); 059 } 060 061 @Override 062 public IBaseResource fetchCodeSystem(String theSystem) { 063 IGenericClient client = provideClient(); 064 Class<? extends IBaseBundle> bundleType = myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class); 065 IBaseBundle results = client 066 .search() 067 .forResource("CodeSystem") 068 .where(CodeSystem.URL.matches().value(theSystem)) 069 .returnBundle(bundleType) 070 .execute(); 071 List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results); 072 if (resultsList.size() > 0) { 073 return resultsList.get(0); 074 } 075 076 return null; 077 } 078 079 @Override 080 public IBaseResource fetchValueSet(String theValueSetUrl) { 081 IGenericClient client = provideClient(); 082 Class<? extends IBaseBundle> bundleType = myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class); 083 IBaseBundle results = client 084 .search() 085 .forResource("ValueSet") 086 .where(CodeSystem.URL.matches().value(theValueSetUrl)) 087 .returnBundle(bundleType) 088 .execute(); 089 List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results); 090 if (resultsList.size() > 0) { 091 return resultsList.get(0); 092 } 093 094 return null; 095 } 096 097 @Override 098 public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { 099 return fetchCodeSystem(theSystem) != null; 100 } 101 102 @Override 103 public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) { 104 return fetchValueSet(theValueSetUrl) != null; 105 } 106 107 private IGenericClient provideClient() { 108 IGenericClient retVal = myCtx.newRestfulGenericClient(myBaseUrl); 109 for (Object next : myClientInterceptors) { 110 retVal.registerInterceptor(next); 111 } 112 return retVal; 113 } 114 115 protected CodeValidationResult invokeRemoteValidateCode(String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, IBaseResource theValueSet) { 116 if (isBlank(theCode)) { 117 return null; 118 } 119 120 IGenericClient client = provideClient(); 121 122 IBaseParameters input = ParametersUtil.newInstance(getFhirContext()); 123 124 String resourceType = "ValueSet"; 125 if (theValueSet == null && theValueSetUrl == null) { 126 resourceType = "CodeSystem"; 127 128 ParametersUtil.addParameterToParametersUri(getFhirContext(), input, "url", theCodeSystem); 129 ParametersUtil.addParameterToParametersString(getFhirContext(), input, "code", theCode); 130 if (isNotBlank(theDisplay)) { 131 ParametersUtil.addParameterToParametersString(getFhirContext(), input, "display", theDisplay); 132 } 133 134 } else { 135 136 if (isNotBlank(theValueSetUrl)) { 137 ParametersUtil.addParameterToParametersUri(getFhirContext(), input, "url", theValueSetUrl); 138 } 139 ParametersUtil.addParameterToParametersString(getFhirContext(), input, "code", theCode); 140 if (isNotBlank(theCodeSystem)) { 141 ParametersUtil.addParameterToParametersUri(getFhirContext(), input, "system", theCodeSystem); 142 } 143 if (isNotBlank(theDisplay)) { 144 ParametersUtil.addParameterToParametersString(getFhirContext(), input, "display", theDisplay); 145 } 146 if (theValueSet != null) { 147 ParametersUtil.addParameterToParameters(getFhirContext(), input, "valueSet", theValueSet); 148 } 149 150 } 151 152 153 IBaseParameters output = client 154 .operation() 155 .onType(resourceType) 156 .named("validate-code") 157 .withParameters(input) 158 .execute(); 159 160 List<String> resultValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "result"); 161 if (resultValues.size() < 1 || isBlank(resultValues.get(0))) { 162 return null; 163 } 164 Validate.isTrue(resultValues.size() == 1, "Response contained %d 'result' values", resultValues.size()); 165 166 boolean success = "true".equalsIgnoreCase(resultValues.get(0)); 167 168 CodeValidationResult retVal = new CodeValidationResult(); 169 if (success) { 170 171 retVal.setCode(theCode); 172 List<String> displayValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "display"); 173 if (displayValues.size() > 0) { 174 retVal.setDisplay(displayValues.get(0)); 175 } 176 177 } else { 178 179 retVal.setSeverity(IssueSeverity.ERROR); 180 List<String> messageValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "message"); 181 if (messageValues.size() > 0) { 182 retVal.setMessage(messageValues.get(0)); 183 } 184 185 } 186 return retVal; 187 } 188 189 /** 190 * Sets the FHIR Terminology Server base URL 191 * 192 * @param theBaseUrl The base URL, e.g. "https://hapi.fhir.org/baseR4" 193 */ 194 public void setBaseUrl(String theBaseUrl) { 195 Validate.notBlank(theBaseUrl, "theBaseUrl must be provided"); 196 myBaseUrl = theBaseUrl; 197 } 198 199 /** 200 * Adds an interceptor that will be registered to all clients. 201 * <p> 202 * Note that this method is not thread-safe and should only be called prior to this module 203 * being used. 204 * </p> 205 * 206 * @param theClientInterceptor The interceptor (must not be null) 207 */ 208 public void addClientInterceptor(@Nonnull Object theClientInterceptor) { 209 Validate.notNull(theClientInterceptor, "theClientInterceptor must not be null"); 210 myClientInterceptors.add(theClientInterceptor); 211 } 212 213}