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