001package org.hl7.fhir.common.hapi.validation.support; 002 003import ca.uhn.fhir.i18n.Msg; 004import ca.uhn.fhir.context.FhirContext; 005import ca.uhn.fhir.context.FhirVersionEnum; 006import ca.uhn.fhir.context.support.ConceptValidationOptions; 007import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; 008import ca.uhn.fhir.context.support.IValidationSupport; 009import ca.uhn.fhir.context.support.ValidationSupportContext; 010import ca.uhn.fhir.rest.client.api.IGenericClient; 011import ca.uhn.fhir.util.BundleUtil; 012import ca.uhn.fhir.util.JsonUtil; 013import ca.uhn.fhir.util.ParametersUtil; 014import org.apache.commons.lang3.StringUtils; 015import org.apache.commons.lang3.Validate; 016import org.hl7.fhir.instance.model.api.IBaseBundle; 017import org.hl7.fhir.instance.model.api.IBaseParameters; 018import org.hl7.fhir.instance.model.api.IBaseResource; 019import org.hl7.fhir.r4.model.CodeSystem; 020import org.hl7.fhir.r4.model.ValueSet; 021import org.slf4j.Logger; 022import org.slf4j.LoggerFactory; 023 024import javax.annotation.Nonnull; 025import java.io.IOException; 026import java.util.ArrayList; 027import java.util.List; 028 029import static org.apache.commons.lang3.StringUtils.isBlank; 030import static org.apache.commons.lang3.StringUtils.isNotBlank; 031 032/** 033 * This class is an implementation of {@link IValidationSupport} that fetches validation codes 034 * from a remote FHIR based terminology server. It will invoke the FHIR 035 * <a href="http://hl7.org/fhir/valueset-operation-validate-code.html">ValueSet/$validate-code</a> 036 * operation in order to validate codes. 037 */ 038public class RemoteTerminologyServiceValidationSupport extends BaseValidationSupport implements IValidationSupport { 039 private static final Logger ourLog = LoggerFactory.getLogger(RemoteTerminologyServiceValidationSupport.class); 040 041 private String myBaseUrl; 042 private List<Object> myClientInterceptors = new ArrayList<>(); 043 044 /** 045 * Constructor 046 * 047 * @param theFhirContext The FhirContext object to use 048 */ 049 public RemoteTerminologyServiceValidationSupport(FhirContext theFhirContext) { 050 super(theFhirContext); 051 } 052 053 public RemoteTerminologyServiceValidationSupport(FhirContext theFhirContext, String theBaseUrl) { 054 super(theFhirContext); 055 myBaseUrl = theBaseUrl; 056 } 057 058 @Override 059 public CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) { 060 return invokeRemoteValidateCode(theCodeSystem, theCode, theDisplay, theValueSetUrl, null); 061 } 062 063 @Override 064 public CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) { 065 066 IBaseResource valueSet = theValueSet; 067 068 // some external validators require the system when the code is passed 069 // so let's try to get it from the VS if is is not present 070 String codeSystem = theCodeSystem; 071 if (isNotBlank(theCode) && isBlank(codeSystem)) { 072 codeSystem = extractCodeSystemForCode((ValueSet) theValueSet, theCode); 073 } 074 075 // Remote terminology services shouldn't be used to validate codes with an implied system 076 if (isBlank(codeSystem)) { return null; } 077 078 String valueSetUrl = DefaultProfileValidationSupport.getConformanceResourceUrl(myCtx, valueSet); 079 if (isNotBlank(valueSetUrl)) { 080 valueSet = null; 081 } else { 082 valueSetUrl = null; 083 } 084 return invokeRemoteValidateCode(codeSystem, theCode, theDisplay, valueSetUrl, valueSet); 085 } 086 087 /** 088 * Try to obtain the codeSystem of the received code from the received ValueSet 089 */ 090 private String extractCodeSystemForCode(ValueSet theValueSet, String theCode) { 091 if (theValueSet.getCompose() == null || theValueSet.getCompose().getInclude() == null 092 || theValueSet.getCompose().getInclude().isEmpty()) { 093 return null; 094 } 095 096 if (theValueSet.getCompose().getInclude().size() == 1) { 097 ValueSet.ConceptSetComponent include = theValueSet.getCompose().getInclude().iterator().next(); 098 return getVersionedCodeSystem(include); 099 } 100 101 // when component has more than one include, their codeSystem(s) could be different, so we need to make sure 102 // that we are picking up the system for the include to which the code corresponds 103 for (ValueSet.ConceptSetComponent include: theValueSet.getCompose().getInclude()) { 104 if (include.hasSystem()) { 105 for (ValueSet.ConceptReferenceComponent concept : include.getConcept()) { 106 if (concept.hasCodeElement() && concept.getCode().equals(theCode)) { 107 return getVersionedCodeSystem(include); 108 } 109 } 110 } 111 } 112 113 // at this point codeSystem couldn't be extracted for a multi-include ValueSet. Just on case it was 114 // because the format was not well handled, let's allow to watch the VS by an easy logging change 115 ourLog.trace("CodeSystem couldn't be extracted for code: {} for ValueSet: {}", theCode, theValueSet.getId()); 116 return null; 117 } 118 119 private String getVersionedCodeSystem(ValueSet.ConceptSetComponent theComponent) { 120 String codeSystem = theComponent.getSystem(); 121 if ( ! codeSystem.contains("|") && theComponent.hasVersion()) { 122 codeSystem += "|" + theComponent.getVersion(); 123 } 124 return codeSystem; 125 } 126 127 @Override 128 public IBaseResource fetchCodeSystem(String theSystem) { 129 IGenericClient client = provideClient(); 130 Class<? extends IBaseBundle> bundleType = myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class); 131 IBaseBundle results = client 132 .search() 133 .forResource("CodeSystem") 134 .where(CodeSystem.URL.matches().value(theSystem)) 135 .returnBundle(bundleType) 136 .execute(); 137 List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results); 138 if (resultsList.size() > 0) { 139 return resultsList.get(0); 140 } 141 142 return null; 143 } 144 145 @Override 146 public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode, String theDisplayLanguage) { 147 Validate.notBlank(theCode, "theCode must be provided"); 148 149 IGenericClient client = provideClient(); 150 FhirContext fhirContext = client.getFhirContext(); 151 FhirVersionEnum fhirVersion = fhirContext.getVersion().getVersion(); 152 153 switch (fhirVersion) { 154 case DSTU3: 155 case R4: 156 IBaseParameters params = ParametersUtil.newInstance(fhirContext); 157 ParametersUtil.addParameterToParametersString(fhirContext, params, "code", theCode); 158 if (!StringUtils.isEmpty(theSystem)) { 159 ParametersUtil.addParameterToParametersString(fhirContext, params, "system", theSystem); 160 } 161 if (!StringUtils.isEmpty(theDisplayLanguage)) { 162 ParametersUtil.addParameterToParametersString(fhirContext, params, "language", theDisplayLanguage); 163 } 164 Class<?> codeSystemClass = myCtx.getResourceDefinition("CodeSystem").getImplementingClass(); 165 IBaseParameters outcome = client 166 .operation() 167 .onType((Class<? extends IBaseResource>) codeSystemClass) 168 .named("$lookup") 169 .withParameters(params) 170 .useHttpGet() 171 .execute(); 172 if (outcome != null && !outcome.isEmpty()) { 173 switch (fhirVersion) { 174 case DSTU3: 175 return generateLookupCodeResultDSTU3(theCode, theSystem, (org.hl7.fhir.dstu3.model.Parameters)outcome); 176 case R4: 177 return generateLookupCodeResultR4(theCode, theSystem, (org.hl7.fhir.r4.model.Parameters)outcome); 178 } 179 } 180 break; 181 default: 182 throw new UnsupportedOperationException(Msg.code(710) + "Unsupported FHIR version '" + fhirVersion.getFhirVersionString() + 183 "'. Only DSTU3 and R4 are supported."); 184 } 185 return null; 186 } 187 188 private LookupCodeResult generateLookupCodeResultDSTU3(String theCode, String theSystem, org.hl7.fhir.dstu3.model.Parameters outcomeDSTU3) { 189 // NOTE: I wanted to put all of this logic into the IValidationSupport Class, but it would've required adding 190 // several new dependencies on version-specific libraries and that is explicitly forbidden (see comment in POM). 191 LookupCodeResult result = new LookupCodeResult(); 192 result.setSearchedForCode(theCode); 193 result.setSearchedForSystem(theSystem); 194 result.setFound(true); 195 for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent parameterComponent : outcomeDSTU3.getParameter()) { 196 switch (parameterComponent.getName()) { 197 case "property": 198 org.hl7.fhir.dstu3.model.Property part = parameterComponent.getChildByName("part"); 199 // The assumption here is that we may only have 2 elements in this part, and if so, these 2 will be saved 200 if (part != null && part.hasValues() && part.getValues().size() >= 2) { 201 String key = ((org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) part.getValues().get(0)).getValue().toString(); 202 String value = ((org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) part.getValues().get(1)).getValue().toString(); 203 if (!StringUtils.isEmpty(key) && !StringUtils.isEmpty(value)) { 204 result.getProperties().add(new StringConceptProperty(key, value)); 205 } 206 } 207 break; 208 case "designation": 209 ConceptDesignation conceptDesignation = new ConceptDesignation(); 210 for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent designationComponent : parameterComponent.getPart()) { 211 switch(designationComponent.getName()) { 212 case "language": 213 conceptDesignation.setLanguage(designationComponent.getValue().toString()); 214 break; 215 case "use": 216 org.hl7.fhir.dstu3.model.Coding coding = (org.hl7.fhir.dstu3.model.Coding)designationComponent.getValue(); 217 if (coding != null) { 218 conceptDesignation.setUseSystem(coding.getSystem()); 219 conceptDesignation.setUseCode(coding.getCode()); 220 conceptDesignation.setUseDisplay(coding.getDisplay()); 221 } 222 break; 223 case "value": 224 conceptDesignation.setValue(((designationComponent.getValue() == null)?null:designationComponent.getValue().toString())); 225 break; 226 } 227 } 228 result.getDesignations().add(conceptDesignation); 229 break; 230 case "name": 231 result.setCodeSystemDisplayName(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString())); 232 break; 233 case "version": 234 result.setCodeSystemVersion(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString())); 235 break; 236 case "display": 237 result.setCodeDisplay(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString())); 238 break; 239 case "abstract": 240 result.setCodeIsAbstract(((parameterComponent.getValue() == null)?false:Boolean.parseBoolean(parameterComponent.getValue().toString()))); 241 break; 242 } 243 } 244 return result; 245 } 246 247 private LookupCodeResult generateLookupCodeResultR4(String theCode, String theSystem, org.hl7.fhir.r4.model.Parameters outcomeR4) { 248 // NOTE: I wanted to put all of this logic into the IValidationSupport Class, but it would've required adding 249 // several new dependencies on version-specific libraries and that is explicitly forbidden (see comment in POM). 250 LookupCodeResult result = new LookupCodeResult(); 251 result.setSearchedForCode(theCode); 252 result.setSearchedForSystem(theSystem); 253 result.setFound(true); 254 for (org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent parameterComponent : outcomeR4.getParameter()) { 255 switch (parameterComponent.getName()) { 256 case "property": 257 org.hl7.fhir.r4.model.Property part = parameterComponent.getChildByName("part"); 258 // The assumption here is that we may only have 2 elements in this part, and if so, these 2 will be saved 259 if (part != null && part.hasValues() && part.getValues().size() >= 2) { 260 String key = ((org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent) part.getValues().get(0)).getValue().toString(); 261 String value = ((org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent) part.getValues().get(1)).getValue().toString(); 262 if (!StringUtils.isEmpty(key) && !StringUtils.isEmpty(value)) { 263 result.getProperties().add(new StringConceptProperty(key, value)); 264 } 265 } 266 break; 267 case "designation": 268 ConceptDesignation conceptDesignation = new ConceptDesignation(); 269 for (org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent designationComponent : parameterComponent.getPart()) { 270 switch(designationComponent.getName()) { 271 case "language": 272 conceptDesignation.setLanguage(designationComponent.getValue().toString()); 273 break; 274 case "use": 275 org.hl7.fhir.r4.model.Coding coding = (org.hl7.fhir.r4.model.Coding)designationComponent.getValue(); 276 if (coding != null) { 277 conceptDesignation.setUseSystem(coding.getSystem()); 278 conceptDesignation.setUseCode(coding.getCode()); 279 conceptDesignation.setUseDisplay(coding.getDisplay()); 280 } 281 break; 282 case "value": 283 conceptDesignation.setValue(((designationComponent.getValue() == null)?null:designationComponent.getValue().toString())); 284 break; 285 } 286 } 287 result.getDesignations().add(conceptDesignation); 288 break; 289 case "name": 290 result.setCodeSystemDisplayName(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString())); 291 break; 292 case "version": 293 result.setCodeSystemVersion(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString())); 294 break; 295 case "display": 296 result.setCodeDisplay(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString())); 297 break; 298 case "abstract": 299 result.setCodeIsAbstract(((parameterComponent.getValue() == null)?false:Boolean.parseBoolean(parameterComponent.getValue().toString()))); 300 break; 301 } 302 } 303 return result; 304 } 305 306 @Override 307 public IBaseResource fetchValueSet(String theValueSetUrl) { 308 IGenericClient client = provideClient(); 309 Class<? extends IBaseBundle> bundleType = myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class); 310 IBaseBundle results = client 311 .search() 312 .forResource("ValueSet") 313 .where(CodeSystem.URL.matches().value(theValueSetUrl)) 314 .returnBundle(bundleType) 315 .execute(); 316 List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results); 317 if (resultsList.size() > 0) { 318 return resultsList.get(0); 319 } 320 321 return null; 322 } 323 324 @Override 325 public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { 326 return fetchCodeSystem(theSystem) != null; 327 } 328 329 @Override 330 public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) { 331 return fetchValueSet(theValueSetUrl) != null; 332 } 333 334 private IGenericClient provideClient() { 335 IGenericClient retVal = myCtx.newRestfulGenericClient(myBaseUrl); 336 for (Object next : myClientInterceptors) { 337 retVal.registerInterceptor(next); 338 } 339 return retVal; 340 } 341 342 protected CodeValidationResult invokeRemoteValidateCode(String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, IBaseResource theValueSet) { 343 if (isBlank(theCode)) { 344 return null; 345 } 346 347 IGenericClient client = provideClient(); 348 349 IBaseParameters input = buildValidateCodeInputParameters(theCodeSystem, theCode, theDisplay, theValueSetUrl, theValueSet); 350 351 String resourceType = "ValueSet"; 352 if (theValueSet == null && theValueSetUrl == null) { 353 resourceType = "CodeSystem"; 354 } 355 356 IBaseParameters output = client 357 .operation() 358 .onType(resourceType) 359 .named("validate-code") 360 .withParameters(input) 361 .execute(); 362 363 List<String> resultValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "result"); 364 if (resultValues.size() < 1 || isBlank(resultValues.get(0))) { 365 return null; 366 } 367 Validate.isTrue(resultValues.size() == 1, "Response contained %d 'result' values", resultValues.size()); 368 369 boolean success = "true".equalsIgnoreCase(resultValues.get(0)); 370 371 CodeValidationResult retVal = new CodeValidationResult(); 372 if (success) { 373 374 retVal.setCode(theCode); 375 List<String> displayValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "display"); 376 if (displayValues.size() > 0) { 377 retVal.setDisplay(displayValues.get(0)); 378 } 379 380 } else { 381 382 retVal.setSeverity(IssueSeverity.ERROR); 383 List<String> messageValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "message"); 384 if (messageValues.size() > 0) { 385 retVal.setMessage(messageValues.get(0)); 386 } 387 388 } 389 return retVal; 390 } 391 392 protected IBaseParameters buildValidateCodeInputParameters(String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, IBaseResource theValueSet) { 393 IBaseParameters params = ParametersUtil.newInstance(getFhirContext()); 394 395 if (theValueSet == null && theValueSetUrl == null) { 396 ParametersUtil.addParameterToParametersUri(getFhirContext(), params, "url", theCodeSystem); 397 ParametersUtil.addParameterToParametersString(getFhirContext(), params, "code", theCode); 398 if (isNotBlank(theDisplay)) { 399 ParametersUtil.addParameterToParametersString(getFhirContext(), params, "display", theDisplay); 400 } 401 return params; 402 } 403 404 if (isNotBlank(theValueSetUrl)) { 405 ParametersUtil.addParameterToParametersUri(getFhirContext(), params, "url", theValueSetUrl); 406 } 407 ParametersUtil.addParameterToParametersString(getFhirContext(), params, "code", theCode); 408 if (isNotBlank(theCodeSystem)) { 409 ParametersUtil.addParameterToParametersUri(getFhirContext(), params, "system", theCodeSystem); 410 } 411 if (isNotBlank(theDisplay)) { 412 ParametersUtil.addParameterToParametersString(getFhirContext(), params, "display", theDisplay); 413 } 414 if (theValueSet != null) { 415 ParametersUtil.addParameterToParameters(getFhirContext(), params, "valueSet", theValueSet); 416 } 417 return params; 418 } 419 420 421 /** 422 * Sets the FHIR Terminology Server base URL 423 * 424 * @param theBaseUrl The base URL, e.g. "https://hapi.fhir.org/baseR4" 425 */ 426 public void setBaseUrl(String theBaseUrl) { 427 Validate.notBlank(theBaseUrl, "theBaseUrl must be provided"); 428 myBaseUrl = theBaseUrl; 429 } 430 431 /** 432 * Adds an interceptor that will be registered to all clients. 433 * <p> 434 * Note that this method is not thread-safe and should only be called prior to this module 435 * being used. 436 * </p> 437 * 438 * @param theClientInterceptor The interceptor (must not be null) 439 */ 440 public void addClientInterceptor(@Nonnull Object theClientInterceptor) { 441 Validate.notNull(theClientInterceptor, "theClientInterceptor must not be null"); 442 myClientInterceptors.add(theClientInterceptor); 443 } 444 445}