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 052 if (theOptions != null) { 053 if (theOptions.isInferSystem()) { 054 return null; 055 } 056 } 057 058 IBaseResource valueSet = theValueSet; 059 String valueSetUrl = DefaultProfileValidationSupport.getConformanceResourceUrl(myCtx, valueSet); 060 if (isNotBlank(valueSetUrl)) { 061 valueSet = null; 062 } else { 063 valueSetUrl = null; 064 } 065 return invokeRemoteValidateCode(theCodeSystem, theCode, theDisplay, valueSetUrl, valueSet); 066 } 067 068 @Override 069 public IBaseResource fetchCodeSystem(String theSystem) { 070 IGenericClient client = provideClient(); 071 Class<? extends IBaseBundle> bundleType = myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class); 072 IBaseBundle results = client 073 .search() 074 .forResource("CodeSystem") 075 .where(CodeSystem.URL.matches().value(theSystem)) 076 .returnBundle(bundleType) 077 .execute(); 078 List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results); 079 if (resultsList.size() > 0) { 080 return resultsList.get(0); 081 } 082 083 return null; 084 } 085 086 @Override 087 public IBaseResource fetchValueSet(String theValueSetUrl) { 088 IGenericClient client = provideClient(); 089 Class<? extends IBaseBundle> bundleType = myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class); 090 IBaseBundle results = client 091 .search() 092 .forResource("ValueSet") 093 .where(CodeSystem.URL.matches().value(theValueSetUrl)) 094 .returnBundle(bundleType) 095 .execute(); 096 List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results); 097 if (resultsList.size() > 0) { 098 return resultsList.get(0); 099 } 100 101 return null; 102 } 103 104 @Override 105 public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { 106 return fetchCodeSystem(theSystem) != null; 107 } 108 109 @Override 110 public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) { 111 return fetchValueSet(theValueSetUrl) != null; 112 } 113 114 private IGenericClient provideClient() { 115 IGenericClient retVal = myCtx.newRestfulGenericClient(myBaseUrl); 116 for (Object next : myClientInterceptors) { 117 retVal.registerInterceptor(next); 118 } 119 return retVal; 120 } 121 122 protected CodeValidationResult invokeRemoteValidateCode(String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, IBaseResource theValueSet) { 123 if (isBlank(theCode)) { 124 return null; 125 } 126 127 IGenericClient client = provideClient(); 128 129 IBaseParameters input = ParametersUtil.newInstance(getFhirContext()); 130 131 String resourceType = "ValueSet"; 132 if (theValueSet == null && theValueSetUrl == null) { 133 resourceType = "CodeSystem"; 134 135 ParametersUtil.addParameterToParametersUri(getFhirContext(), input, "url", theCodeSystem); 136 ParametersUtil.addParameterToParametersString(getFhirContext(), input, "code", theCode); 137 if (isNotBlank(theDisplay)) { 138 ParametersUtil.addParameterToParametersString(getFhirContext(), input, "display", theDisplay); 139 } 140 141 } else { 142 143 if (isNotBlank(theValueSetUrl)) { 144 ParametersUtil.addParameterToParametersUri(getFhirContext(), input, "url", theValueSetUrl); 145 } 146 ParametersUtil.addParameterToParametersString(getFhirContext(), input, "code", theCode); 147 if (isNotBlank(theCodeSystem)) { 148 ParametersUtil.addParameterToParametersUri(getFhirContext(), input, "system", theCodeSystem); 149 } 150 if (isNotBlank(theDisplay)) { 151 ParametersUtil.addParameterToParametersString(getFhirContext(), input, "display", theDisplay); 152 } 153 if (theValueSet != null) { 154 ParametersUtil.addParameterToParameters(getFhirContext(), input, "valueSet", theValueSet); 155 } 156 157 } 158 159 160 IBaseParameters output = client 161 .operation() 162 .onType(resourceType) 163 .named("validate-code") 164 .withParameters(input) 165 .execute(); 166 167 List<String> resultValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "result"); 168 if (resultValues.size() < 1 || isBlank(resultValues.get(0))) { 169 return null; 170 } 171 Validate.isTrue(resultValues.size() == 1, "Response contained %d 'result' values", resultValues.size()); 172 173 boolean success = "true".equalsIgnoreCase(resultValues.get(0)); 174 175 CodeValidationResult retVal = new CodeValidationResult(); 176 if (success) { 177 178 retVal.setCode(theCode); 179 List<String> displayValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "display"); 180 if (displayValues.size() > 0) { 181 retVal.setDisplay(displayValues.get(0)); 182 } 183 184 } else { 185 186 retVal.setSeverity(IssueSeverity.ERROR); 187 List<String> messageValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "message"); 188 if (messageValues.size() > 0) { 189 retVal.setMessage(messageValues.get(0)); 190 } 191 192 } 193 return retVal; 194 } 195 196 /** 197 * Sets the FHIR Terminology Server base URL 198 * 199 * @param theBaseUrl The base URL, e.g. "https://hapi.fhir.org/baseR4" 200 */ 201 public void setBaseUrl(String theBaseUrl) { 202 Validate.notBlank(theBaseUrl, "theBaseUrl must be provided"); 203 myBaseUrl = theBaseUrl; 204 } 205 206 /** 207 * Adds an interceptor that will be registered to all clients. 208 * <p> 209 * Note that this method is not thread-safe and should only be called prior to this module 210 * being used. 211 * </p> 212 * 213 * @param theClientInterceptor The interceptor (must not be null) 214 */ 215 public void addClientInterceptor(@Nonnull Object theClientInterceptor) { 216 Validate.notNull(theClientInterceptor, "theClientInterceptor must not be null"); 217 myClientInterceptors.add(theClientInterceptor); 218 } 219 220}