
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.LookupCodeRequest; 009import ca.uhn.fhir.context.support.TranslateConceptResults; 010import ca.uhn.fhir.context.support.ValidationSupportContext; 011import ca.uhn.fhir.i18n.Msg; 012import ca.uhn.fhir.rest.api.SummaryEnum; 013import ca.uhn.fhir.rest.client.api.IGenericClient; 014import ca.uhn.fhir.rest.client.api.IRestfulClientFactory; 015import ca.uhn.fhir.rest.gclient.IQuery; 016import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 017import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 018import ca.uhn.fhir.util.BundleUtil; 019import ca.uhn.fhir.util.Logs; 020import ca.uhn.fhir.util.ParametersUtil; 021import jakarta.annotation.Nonnull; 022import jakarta.annotation.Nullable; 023import org.apache.commons.lang3.StringUtils; 024import org.apache.commons.lang3.Validate; 025import org.hl7.fhir.instance.model.api.IBaseBundle; 026import org.hl7.fhir.instance.model.api.IBaseDatatype; 027import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 028import org.hl7.fhir.instance.model.api.IBaseParameters; 029import org.hl7.fhir.instance.model.api.IBaseResource; 030import org.hl7.fhir.r4.model.Base; 031import org.hl7.fhir.r4.model.BooleanType; 032import org.hl7.fhir.r4.model.CodeSystem; 033import org.hl7.fhir.r4.model.CodeType; 034import org.hl7.fhir.r4.model.CodeableConcept; 035import org.hl7.fhir.r4.model.Coding; 036import org.hl7.fhir.r4.model.OperationOutcome; 037import org.hl7.fhir.r4.model.Parameters; 038import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; 039import org.hl7.fhir.r4.model.Property; 040import org.hl7.fhir.r4.model.StringType; 041import org.hl7.fhir.r4.model.Type; 042import org.slf4j.Logger; 043 044import java.util.ArrayList; 045import java.util.Collection; 046import java.util.List; 047import java.util.Objects; 048import java.util.Optional; 049import java.util.stream.Collectors; 050 051import static ca.uhn.fhir.util.ParametersUtil.getNamedParameterResource; 052import static ca.uhn.fhir.util.ParametersUtil.getNamedParameterValueAsString; 053import static org.apache.commons.lang3.StringUtils.isBlank; 054import static org.apache.commons.lang3.StringUtils.isNotBlank; 055 056/** 057 * This class is an implementation of {@link IValidationSupport} that fetches validation codes 058 * from a remote FHIR based terminology server. It will invoke the FHIR 059 * <a href="http://hl7.org/fhir/valueset-operation-validate-code.html">ValueSet/$validate-code</a> 060 * operation in order to validate codes. 061 */ 062public class RemoteTerminologyServiceValidationSupport extends BaseValidationSupport implements IValidationSupport { 063 private static final Logger ourLog = Logs.getTerminologyTroubleshootingLog(); 064 065 public static final String ERROR_CODE_UNKNOWN_CODE_IN_CODE_SYSTEM = "unknownCodeInSystem"; 066 public static final String ERROR_CODE_UNKNOWN_CODE_IN_VALUE_SET = "unknownCodeInValueSet"; 067 068 private String myBaseUrl; 069 private final List<Object> myClientInterceptors = new ArrayList<>(); 070 071 @Nullable 072 private final IRestfulClientFactory myRestfulClientFactory; 073 074 /** 075 * Constructor 076 * 077 * @param theFhirContext The FhirContext. Will be used to create a FHIR client for remote terminology requests. 078 */ 079 public RemoteTerminologyServiceValidationSupport(FhirContext theFhirContext) { 080 this(theFhirContext, null); 081 } 082 083 /** 084 * Constructor 085 * 086 * @param theFhirContext The FhirContext. Will be used to create a FHIR client for remote terminology requests. 087 * @param theBaseUrl The url used for the remote terminology FHIR client. 088 */ 089 public RemoteTerminologyServiceValidationSupport(FhirContext theFhirContext, String theBaseUrl) { 090 this(theFhirContext, theBaseUrl, null); 091 } 092 093 /** 094 * Constructor 095 * 096 * @param theFhirContext The FhirContext. 097 * @param theBaseUrl The url used for the remote terminology FHIR client. 098 * @param theRestfulClientFactory Used to create the remote terminology FHIR client. If this is not supplied, a client will be created from the FhirContext 099 */ 100 public RemoteTerminologyServiceValidationSupport( 101 FhirContext theFhirContext, String theBaseUrl, @Nullable IRestfulClientFactory theRestfulClientFactory) { 102 super(theFhirContext); 103 myBaseUrl = theBaseUrl; 104 myRestfulClientFactory = theRestfulClientFactory; 105 } 106 107 @Override 108 public String getName() { 109 return getFhirContext().getVersion().getVersion() + " Remote Terminology Service Validation Support"; 110 } 111 112 @Override 113 public CodeValidationResult validateCode( 114 ValidationSupportContext theValidationSupportContext, 115 ConceptValidationOptions theOptions, 116 String theCodeSystem, 117 String theCode, 118 String theDisplay, 119 String theValueSetUrl) { 120 121 return invokeRemoteValidateCode(theCodeSystem, theCode, theDisplay, theValueSetUrl, null); 122 } 123 124 @Override 125 public CodeValidationResult validateCodeInValueSet( 126 ValidationSupportContext theValidationSupportContext, 127 ConceptValidationOptions theOptions, 128 String theCodeSystem, 129 String theCode, 130 String theDisplay, 131 @Nonnull IBaseResource theValueSet) { 132 133 IBaseResource valueSet = theValueSet; 134 135 // some external validators require the system when the code is passed 136 // so let's try to get it from the VS if is not present 137 String codeSystem = theCodeSystem; 138 if (isNotBlank(theCode) && isBlank(codeSystem)) { 139 codeSystem = ValidationSupportUtils.extractCodeSystemForCode(theValueSet, theCode); 140 } 141 142 String valueSetUrl = DefaultProfileValidationSupport.getConformanceResourceUrl(myCtx, valueSet); 143 if (isNotBlank(valueSetUrl)) { 144 valueSet = null; 145 } else { 146 valueSetUrl = null; 147 } 148 return invokeRemoteValidateCode(codeSystem, theCode, theDisplay, valueSetUrl, valueSet); 149 } 150 151 @Override 152 public IBaseResource fetchCodeSystem(String theSystem) { 153 // callers of this want the whole resource. 154 return fetchCodeSystem(theSystem, SummaryEnum.FALSE); 155 } 156 157 /** 158 * Fetch the code system, possibly a summary. 159 * @param theSystem the canonical url 160 * @param theSummaryParam to force a summary mode - or null to allow server default. 161 * @return the CodeSystem 162 */ 163 @Nullable 164 private IBaseResource fetchCodeSystem(String theSystem, @Nullable SummaryEnum theSummaryParam) { 165 IGenericClient client = provideClient(); 166 Class<? extends IBaseBundle> bundleType = 167 myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class); 168 IQuery<IBaseBundle> codeSystemQuery = client.search() 169 .forResource("CodeSystem") 170 .where(CodeSystem.URL.matches().value(theSystem)); 171 172 if (theSummaryParam != null) { 173 codeSystemQuery.summaryMode(theSummaryParam); 174 } 175 176 IBaseBundle results = codeSystemQuery.returnBundle(bundleType).execute(); 177 List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results); 178 if (!resultsList.isEmpty()) { 179 return resultsList.get(0); 180 } 181 182 return null; 183 } 184 185 @Override 186 public LookupCodeResult lookupCode( 187 ValidationSupportContext theValidationSupportContext, @Nonnull LookupCodeRequest theLookupCodeRequest) { 188 final String code = theLookupCodeRequest.getCode(); 189 final String system = theLookupCodeRequest.getSystem(); 190 final String displayLanguage = theLookupCodeRequest.getDisplayLanguage(); 191 Validate.notBlank(code, "theCode must be provided"); 192 193 IGenericClient client = provideClient(); 194 FhirContext fhirContext = client.getFhirContext(); 195 FhirVersionEnum fhirVersion = fhirContext.getVersion().getVersion(); 196 197 if (fhirVersion.isNewerThan(FhirVersionEnum.R4) || fhirVersion.isOlderThan(FhirVersionEnum.DSTU3)) { 198 throw new UnsupportedOperationException(Msg.code(710) + "Unsupported FHIR version '" 199 + fhirVersion.getFhirVersionString() + "'. Only DSTU3 and R4 are supported."); 200 } 201 202 IBaseParameters params = ParametersUtil.newInstance(fhirContext); 203 ParametersUtil.addParameterToParametersString(fhirContext, params, "code", code); 204 if (!StringUtils.isEmpty(system)) { 205 ParametersUtil.addParameterToParametersString(fhirContext, params, "system", system); 206 } 207 if (!StringUtils.isEmpty(displayLanguage)) { 208 ParametersUtil.addParameterToParametersString(fhirContext, params, "language", displayLanguage); 209 } 210 for (String propertyName : theLookupCodeRequest.getPropertyNames()) { 211 ParametersUtil.addParameterToParametersCode(fhirContext, params, "property", propertyName); 212 } 213 Class<? extends IBaseResource> codeSystemClass = 214 myCtx.getResourceDefinition("CodeSystem").getImplementingClass(); 215 IBaseParameters outcome; 216 try { 217 outcome = client.operation() 218 .onType(codeSystemClass) 219 .named("$lookup") 220 .withParameters(params) 221 .useHttpGet() 222 .execute(); 223 } catch (ResourceNotFoundException | InvalidRequestException e) { 224 // this can potentially be moved to an interceptor and be reused in other areas 225 // where we call a remote server or by the client as a custom interceptor 226 // that interceptor would alter the status code of the response and the body into a different format 227 // e.g. ClientResponseInterceptorModificationTemplate 228 ourLog.error(e.getMessage(), e); 229 LookupCodeResult result = LookupCodeResult.notFound(system, code); 230 result.setErrorMessage(getErrorMessage( 231 ERROR_CODE_UNKNOWN_CODE_IN_CODE_SYSTEM, system, code, getBaseUrl(), e.getMessage())); 232 return result; 233 } 234 if (outcome != null && !outcome.isEmpty()) { 235 if (fhirVersion == FhirVersionEnum.DSTU3) { 236 return generateLookupCodeResultDstu3(code, system, (org.hl7.fhir.dstu3.model.Parameters) outcome); 237 } 238 if (fhirVersion == FhirVersionEnum.R4) { 239 return generateLookupCodeResultR4(code, system, (Parameters) outcome); 240 } 241 } 242 return LookupCodeResult.notFound(system, code); 243 } 244 245 protected String getErrorMessage(String errorCode, Object... theParams) { 246 return getFhirContext().getLocalizer().getMessage(getClass(), errorCode, theParams); 247 } 248 249 private LookupCodeResult generateLookupCodeResultDstu3( 250 String theCode, String theSystem, org.hl7.fhir.dstu3.model.Parameters outcomeDSTU3) { 251 // NOTE: I wanted to put all of this logic into the IValidationSupport Class, but it would've required adding 252 // several new dependencies on version-specific libraries and that is explicitly forbidden (see comment in 253 // POM). 254 LookupCodeResult result = new LookupCodeResult(); 255 result.setSearchedForCode(theCode); 256 result.setSearchedForSystem(theSystem); 257 result.setFound(true); 258 for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent parameterComponent : 259 outcomeDSTU3.getParameter()) { 260 String parameterTypeAsString = Objects.toString(parameterComponent.getValue(), null); 261 switch (parameterComponent.getName()) { 262 case "property": 263 BaseConceptProperty conceptProperty = createConceptPropertyDstu3(parameterComponent); 264 if (conceptProperty != null) { 265 result.getProperties().add(conceptProperty); 266 } 267 break; 268 case "designation": 269 ConceptDesignation conceptDesignation = createConceptDesignationDstu3(parameterComponent); 270 result.getDesignations().add(conceptDesignation); 271 break; 272 case "name": 273 result.setCodeSystemDisplayName(parameterTypeAsString); 274 break; 275 case "version": 276 result.setCodeSystemVersion(parameterTypeAsString); 277 break; 278 case "display": 279 result.setCodeDisplay(parameterTypeAsString); 280 break; 281 case "abstract": 282 result.setCodeIsAbstract(Boolean.parseBoolean(parameterTypeAsString)); 283 break; 284 default: 285 } 286 } 287 return result; 288 } 289 290 private static BaseConceptProperty createConceptPropertyDstu3( 291 org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent theParameterComponent) { 292 org.hl7.fhir.dstu3.model.Property property = theParameterComponent.getChildByName("part"); 293 294 // The assumption here is that we may at east 2 elements in this part 295 if (property == null || property.getValues().size() < 2) { 296 return null; 297 } 298 299 List<org.hl7.fhir.dstu3.model.Base> values = property.getValues(); 300 org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent firstPart = 301 (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) values.get(0); 302 String propertyName = ((org.hl7.fhir.dstu3.model.CodeType) firstPart.getValue()).getValue(); 303 304 org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent secondPart = 305 (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) values.get(1); 306 org.hl7.fhir.dstu3.model.Type value = secondPart.getValue(); 307 308 if (value != null) { 309 return createConceptPropertyDstu3(propertyName, value); 310 } 311 312 String groupName = secondPart.getName(); 313 if (!"subproperty".equals(groupName)) { 314 return null; 315 } 316 317 // handle property group (a property containing sub-properties) 318 GroupConceptProperty groupConceptProperty = new GroupConceptProperty(propertyName); 319 320 // we already retrieved the property name (group name) as first element, next will be the sub-properties. 321 // there is no dedicated value for a property group as it is an aggregate 322 for (int i = 1; i < values.size(); i++) { 323 org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent nextPart = 324 (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) values.get(i); 325 BaseConceptProperty subProperty = createConceptPropertyDstu3(nextPart); 326 if (subProperty != null) { 327 groupConceptProperty.addSubProperty(subProperty); 328 } 329 } 330 return groupConceptProperty; 331 } 332 333 public static BaseConceptProperty createConceptProperty(final String theName, final IBaseDatatype theValue) { 334 if (theValue instanceof Type) { 335 return createConceptPropertyR4(theName, (Type) theValue); 336 } 337 if (theValue instanceof org.hl7.fhir.dstu3.model.Type) { 338 return createConceptPropertyDstu3(theName, (org.hl7.fhir.dstu3.model.Type) theValue); 339 } 340 return null; 341 } 342 343 private static BaseConceptProperty createConceptPropertyDstu3( 344 final String theName, final org.hl7.fhir.dstu3.model.Type theValue) { 345 if (theValue == null) { 346 return null; 347 } 348 BaseConceptProperty conceptProperty; 349 String fhirType = theValue.fhirType(); 350 switch (fhirType) { 351 case IValidationSupport.TYPE_STRING: 352 org.hl7.fhir.dstu3.model.StringType stringType = (org.hl7.fhir.dstu3.model.StringType) theValue; 353 conceptProperty = new StringConceptProperty(theName, stringType.getValue()); 354 break; 355 case IValidationSupport.TYPE_CODING: 356 org.hl7.fhir.dstu3.model.Coding coding = (org.hl7.fhir.dstu3.model.Coding) theValue; 357 conceptProperty = 358 new CodingConceptProperty(theName, coding.getSystem(), coding.getCode(), coding.getDisplay()); 359 break; 360 // TODO: add other property types as per FHIR spec https://github.com/hapifhir/hapi-fhir/issues/5699 361 default: 362 // other types will not fail for Remote Terminology 363 conceptProperty = new StringConceptProperty(theName, theValue.toString()); 364 } 365 return conceptProperty; 366 } 367 368 private ConceptDesignation createConceptDesignationDstu3( 369 org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent theParameterComponent) { 370 ConceptDesignation conceptDesignation = new ConceptDesignation(); 371 for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent designationComponent : 372 theParameterComponent.getPart()) { 373 org.hl7.fhir.dstu3.model.Type designationComponentValue = designationComponent.getValue(); 374 if (designationComponentValue == null) { 375 continue; 376 } 377 switch (designationComponent.getName()) { 378 case "language": 379 conceptDesignation.setLanguage(designationComponentValue.toString()); 380 break; 381 case "use": 382 org.hl7.fhir.dstu3.model.Coding coding = 383 (org.hl7.fhir.dstu3.model.Coding) designationComponentValue; 384 conceptDesignation.setUseSystem(coding.getSystem()); 385 conceptDesignation.setUseCode(coding.getCode()); 386 conceptDesignation.setUseDisplay(coding.getDisplay()); 387 break; 388 case "value": 389 conceptDesignation.setValue(designationComponent.getValue().toString()); 390 break; 391 default: 392 } 393 } 394 return conceptDesignation; 395 } 396 397 private LookupCodeResult generateLookupCodeResultR4(String theCode, String theSystem, Parameters outcomeR4) { 398 // NOTE: I wanted to put all of this logic into the IValidationSupport Class, but it would've required adding 399 // several new dependencies on version-specific libraries and that is explicitly forbidden (see comment in 400 // POM). 401 LookupCodeResult result = new LookupCodeResult(); 402 result.setSearchedForCode(theCode); 403 result.setSearchedForSystem(theSystem); 404 result.setFound(true); 405 for (ParametersParameterComponent parameterComponent : outcomeR4.getParameter()) { 406 String parameterTypeAsString = Objects.toString(parameterComponent.getValue(), null); 407 switch (parameterComponent.getName()) { 408 case "property": 409 BaseConceptProperty conceptProperty = createConceptPropertyR4(parameterComponent); 410 if (conceptProperty != null) { 411 result.getProperties().add(conceptProperty); 412 } 413 break; 414 case "designation": 415 ConceptDesignation conceptDesignation = createConceptDesignationR4(parameterComponent); 416 result.getDesignations().add(conceptDesignation); 417 break; 418 case "name": 419 result.setCodeSystemDisplayName(parameterTypeAsString); 420 break; 421 case "version": 422 result.setCodeSystemVersion(parameterTypeAsString); 423 break; 424 case "display": 425 result.setCodeDisplay(parameterTypeAsString); 426 break; 427 case "abstract": 428 result.setCodeIsAbstract(Boolean.parseBoolean(parameterTypeAsString)); 429 break; 430 default: 431 } 432 } 433 return result; 434 } 435 436 private static BaseConceptProperty createConceptPropertyR4(ParametersParameterComponent thePropertyComponent) { 437 Property property = thePropertyComponent.getChildByName("part"); 438 439 // The assumption here is that we may at east 2 elements in this part 440 if (property == null || property.getValues().size() < 2) { 441 return null; 442 } 443 444 List<Base> values = property.getValues(); 445 ParametersParameterComponent firstPart = (ParametersParameterComponent) values.get(0); 446 String propertyName = ((CodeType) firstPart.getValue()).getValue(); 447 448 ParametersParameterComponent secondPart = (ParametersParameterComponent) values.get(1); 449 Type value = secondPart.getValue(); 450 451 if (value != null) { 452 return createConceptPropertyR4(propertyName, value); 453 } 454 455 String groupName = secondPart.getName(); 456 if (!"subproperty".equals(groupName)) { 457 return null; 458 } 459 460 // handle property group (a property containing sub-properties) 461 GroupConceptProperty groupConceptProperty = new GroupConceptProperty(propertyName); 462 463 // we already retrieved the property name (group name) as first element, next will be the sub-properties. 464 // there is no dedicated value for a property group as it is an aggregate 465 for (int i = 1; i < values.size(); i++) { 466 ParametersParameterComponent nextPart = (ParametersParameterComponent) values.get(i); 467 BaseConceptProperty subProperty = createConceptPropertyR4(nextPart); 468 if (subProperty != null) { 469 groupConceptProperty.addSubProperty(subProperty); 470 } 471 } 472 return groupConceptProperty; 473 } 474 475 private static BaseConceptProperty createConceptPropertyR4(final String theName, final Type theValue) { 476 BaseConceptProperty conceptProperty; 477 478 String fhirType = theValue.fhirType(); 479 switch (fhirType) { 480 case IValidationSupport.TYPE_STRING: 481 StringType stringType = (StringType) theValue; 482 conceptProperty = new StringConceptProperty(theName, stringType.getValue()); 483 break; 484 case IValidationSupport.TYPE_BOOLEAN: 485 BooleanType booleanType = (BooleanType) theValue; 486 conceptProperty = new BooleanConceptProperty(theName, booleanType.getValue()); 487 break; 488 case IValidationSupport.TYPE_CODING: 489 Coding coding = (Coding) theValue; 490 conceptProperty = 491 new CodingConceptProperty(theName, coding.getSystem(), coding.getCode(), coding.getDisplay()); 492 break; 493 // TODO: add other property types as per FHIR spec https://github.com/hapifhir/hapi-fhir/issues/5699 494 default: 495 // other types will not fail for Remote Terminology 496 conceptProperty = new StringConceptProperty(theName, theValue.toString()); 497 } 498 return conceptProperty; 499 } 500 501 private ConceptDesignation createConceptDesignationR4(ParametersParameterComponent theParameterComponent) { 502 ConceptDesignation conceptDesignation = new ConceptDesignation(); 503 for (ParametersParameterComponent designationComponent : theParameterComponent.getPart()) { 504 Type designationComponentValue = designationComponent.getValue(); 505 if (designationComponentValue == null) { 506 continue; 507 } 508 switch (designationComponent.getName()) { 509 case "language": 510 conceptDesignation.setLanguage(designationComponentValue.toString()); 511 break; 512 case "use": 513 Coding coding = (Coding) designationComponentValue; 514 conceptDesignation.setUseSystem(coding.getSystem()); 515 conceptDesignation.setUseCode(coding.getCode()); 516 conceptDesignation.setUseDisplay(coding.getDisplay()); 517 break; 518 case "value": 519 conceptDesignation.setValue(designationComponentValue.toString()); 520 break; 521 default: 522 } 523 } 524 return conceptDesignation; 525 } 526 527 @Override 528 public IBaseResource fetchValueSet(String theValueSetUrl) { 529 // force the remote server to send the whole resource. 530 SummaryEnum summaryParam = SummaryEnum.FALSE; 531 return fetchValueSet(theValueSetUrl, summaryParam); 532 } 533 534 /** 535 * Search for a ValueSet by canonical url via IGenericClient. 536 * 537 * @param theValueSetUrl the canonical url of the ValueSet 538 * @param theSummaryParam force a summary mode - null allows server default 539 * @return the ValueSet or null if none match the url 540 */ 541 @Nullable 542 private IBaseResource fetchValueSet(String theValueSetUrl, SummaryEnum theSummaryParam) { 543 IGenericClient client = provideClient(); 544 Class<? extends IBaseBundle> bundleType = 545 myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class); 546 547 IQuery<IBaseBundle> valueSetQuery = client.search() 548 .forResource("ValueSet") 549 .where(CodeSystem.URL.matches().value(theValueSetUrl)); 550 551 if (theSummaryParam != null) { 552 valueSetQuery.summaryMode(theSummaryParam); 553 } 554 555 IBaseBundle results = valueSetQuery.returnBundle(bundleType).execute(); 556 557 List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results); 558 if (!resultsList.isEmpty()) { 559 return resultsList.get(0); 560 } 561 562 return null; 563 } 564 565 @Override 566 public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { 567 // a summary is ok if we are just checking the presence. 568 SummaryEnum summaryParam = null; 569 570 return fetchCodeSystem(theSystem, summaryParam) != null; 571 } 572 573 @Override 574 public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) { 575 // a summary is ok if we are just checking the presence. 576 SummaryEnum summaryParam = null; 577 578 return fetchValueSet(theValueSetUrl, summaryParam) != null; 579 } 580 581 @Override 582 public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) { 583 IGenericClient client = provideClient(); 584 FhirContext fhirContext = client.getFhirContext(); 585 586 IBaseParameters params = RemoteTerminologyUtil.buildTranslateInputParameters(fhirContext, theRequest); 587 588 IBaseParameters outcome = client.operation() 589 .onType("ConceptMap") 590 .named("$translate") 591 .withParameters(params) 592 .execute(); 593 594 return RemoteTerminologyUtil.translateOutcomeToResults(fhirContext, outcome); 595 } 596 597 private IGenericClient provideClient() { 598 IGenericClient retVal; 599 if (myRestfulClientFactory != null) { 600 retVal = myRestfulClientFactory.newGenericClient(myBaseUrl); 601 } else { 602 retVal = myCtx.newRestfulGenericClient(myBaseUrl); 603 } 604 for (Object next : myClientInterceptors) { 605 retVal.registerInterceptor(next); 606 } 607 return retVal; 608 } 609 610 public String getBaseUrl() { 611 return myBaseUrl; 612 } 613 614 protected CodeValidationResult invokeRemoteValidateCode( 615 String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, IBaseResource theValueSet) { 616 if (isBlank(theCode)) { 617 return null; 618 } 619 620 IGenericClient client = provideClient(); 621 622 // this message builder can be removed once we introduce a parameter object like CodeValidationRequest 623 ValidationErrorMessageBuilder errorMessageBuilder = theServerMessage -> { 624 if (theValueSetUrl == null && theValueSet == null) { 625 return getErrorMessage( 626 ERROR_CODE_UNKNOWN_CODE_IN_CODE_SYSTEM, theCodeSystem, theCode, getBaseUrl(), theServerMessage); 627 } 628 return getErrorMessage( 629 ERROR_CODE_UNKNOWN_CODE_IN_VALUE_SET, 630 theCodeSystem, 631 theCode, 632 theValueSetUrl, 633 getBaseUrl(), 634 theServerMessage); 635 }; 636 637 IBaseParameters input = 638 buildValidateCodeInputParameters(theCodeSystem, theCode, theDisplay, theValueSetUrl, theValueSet); 639 640 String resourceType = "ValueSet"; 641 if (theValueSet == null && theValueSetUrl == null) { 642 resourceType = "CodeSystem"; 643 } 644 645 try { 646 IBaseParameters output = client.operation() 647 .onType(resourceType) 648 .named("validate-code") 649 .withParameters(input) 650 .execute(); 651 return createCodeValidationResult(output, errorMessageBuilder, theCode); 652 } catch (ResourceNotFoundException | InvalidRequestException ex) { 653 ourLog.error(ex.getMessage(), ex); 654 String errorMessage = errorMessageBuilder.buildErrorMessage(ex.getMessage()); 655 CodeValidationIssueCode issueCode = ex instanceof ResourceNotFoundException 656 ? CodeValidationIssueCode.NOT_FOUND 657 : CodeValidationIssueCode.CODE_INVALID; 658 return createErrorCodeValidationResult(issueCode, errorMessage); 659 } 660 } 661 662 private CodeValidationResult createErrorCodeValidationResult( 663 CodeValidationIssueCode theIssueCode, String theMessage) { 664 IssueSeverity severity = IssueSeverity.ERROR; 665 return new CodeValidationResult() 666 .setSeverity(severity) 667 .setMessage(theMessage) 668 .addIssue(new CodeValidationIssue( 669 theMessage, severity, theIssueCode, CodeValidationIssueCoding.INVALID_CODE)); 670 } 671 672 private CodeValidationResult createCodeValidationResult( 673 IBaseParameters theOutput, ValidationErrorMessageBuilder theMessageBuilder, String theCode) { 674 final FhirContext fhirContext = getFhirContext(); 675 Optional<String> resultValue = getNamedParameterValueAsString(fhirContext, theOutput, "result"); 676 677 if (!resultValue.isPresent()) { 678 throw new IllegalArgumentException( 679 Msg.code(2560) + "Parameter `result` is missing from the $validate-code response."); 680 } 681 682 boolean success = resultValue.get().equalsIgnoreCase("true"); 683 684 CodeValidationResult result = new CodeValidationResult(); 685 686 // TODO MM: avoid passing the code and only retrieve it from the response 687 // that implies larger changes, like adding the result boolean to CodeValidationResult 688 // since CodeValidationResult#isOk() relies on code being populated to determine the result/success 689 if (success) { 690 result.setCode(theCode); 691 } 692 693 Optional<String> systemValue = getNamedParameterValueAsString(fhirContext, theOutput, "system"); 694 systemValue.ifPresent(result::setCodeSystemName); 695 Optional<String> versionValue = getNamedParameterValueAsString(fhirContext, theOutput, "version"); 696 versionValue.ifPresent(result::setCodeSystemVersion); 697 Optional<String> displayValue = getNamedParameterValueAsString(fhirContext, theOutput, "display"); 698 displayValue.ifPresent(result::setDisplay); 699 700 // in theory the message and the issues should not be populated when result=false 701 if (success) { 702 return result; 703 } 704 705 // for now assume severity ERROR, we may need to process the following for success cases as well 706 result.setSeverity(IssueSeverity.ERROR); 707 708 Optional<String> messageValue = getNamedParameterValueAsString(fhirContext, theOutput, "message"); 709 messageValue.ifPresent(value -> result.setMessage(theMessageBuilder.buildErrorMessage(value))); 710 711 Optional<IBaseResource> issuesValue = getNamedParameterResource(fhirContext, theOutput, "issues"); 712 if (issuesValue.isPresent()) { 713 // it seems to be safe to cast to IBaseOperationOutcome as any other type would not reach this point 714 createCodeValidationIssues( 715 (IBaseOperationOutcome) issuesValue.get(), 716 fhirContext.getVersion().getVersion()) 717 .ifPresent(i -> i.forEach(result::addIssue)); 718 } else { 719 // create a validation issue out of the message 720 // this is a workaround to overcome an issue in the FHIR Validator library 721 // where ValueSet bindings are only reading issues but not messages 722 // @see https://github.com/hapifhir/org.hl7.fhir.core/issues/1766 723 result.addIssue(createCodeValidationIssue(result.getMessage())); 724 } 725 return result; 726 } 727 728 /** 729 * Creates a list of {@link ca.uhn.fhir.context.support.IValidationSupport.CodeValidationIssue} from the issues 730 * returned by the $validate-code operation. 731 * Please note that this method should only be used for Remote Terminology for now as it only translates 732 * issues text/message and assumes all other fields. 733 * When issues will be supported across all validators in hapi-fhir, a proper generic conversion method should 734 * be available and this method will be deleted. 735 * 736 * @param theOperationOutcome the outcome of the $validate-code operation 737 * @param theFhirVersion the FHIR version 738 * @return the list of {@link ca.uhn.fhir.context.support.IValidationSupport.CodeValidationIssue} 739 */ 740 public static Optional<Collection<CodeValidationIssue>> createCodeValidationIssues( 741 IBaseOperationOutcome theOperationOutcome, FhirVersionEnum theFhirVersion) { 742 if (theFhirVersion == FhirVersionEnum.R4) { 743 return Optional.of(createCodeValidationIssuesR4((OperationOutcome) theOperationOutcome)); 744 } 745 if (theFhirVersion == FhirVersionEnum.DSTU3) { 746 return Optional.of( 747 createCodeValidationIssuesDstu3((org.hl7.fhir.dstu3.model.OperationOutcome) theOperationOutcome)); 748 } 749 return Optional.empty(); 750 } 751 752 private static Collection<CodeValidationIssue> createCodeValidationIssuesR4(OperationOutcome theOperationOutcome) { 753 return theOperationOutcome.getIssue().stream() 754 .map(issueComponent -> { 755 String diagnostics = issueComponent.getDiagnostics(); 756 IssueSeverity issueSeverity = 757 IssueSeverity.fromCode(issueComponent.getSeverity().toCode()); 758 String issueTypeCode = issueComponent.getCode().toCode(); 759 CodeableConcept details = issueComponent.getDetails(); 760 CodeValidationIssue issue = new CodeValidationIssue(diagnostics, issueSeverity, issueTypeCode); 761 CodeValidationIssueDetails issueDetails = new CodeValidationIssueDetails(details.getText()); 762 details.getCoding().forEach(coding -> issueDetails.addCoding(coding.getSystem(), coding.getCode())); 763 issue.setDetails(issueDetails); 764 return issue; 765 }) 766 .collect(Collectors.toList()); 767 } 768 769 private static Collection<CodeValidationIssue> createCodeValidationIssuesDstu3( 770 org.hl7.fhir.dstu3.model.OperationOutcome theOperationOutcome) { 771 return theOperationOutcome.getIssue().stream() 772 .map(issueComponent -> { 773 String diagnostics = issueComponent.getDiagnostics(); 774 IssueSeverity issueSeverity = 775 IssueSeverity.fromCode(issueComponent.getSeverity().toCode()); 776 String issueTypeCode = issueComponent.getCode().toCode(); 777 org.hl7.fhir.dstu3.model.CodeableConcept details = issueComponent.getDetails(); 778 CodeValidationIssue issue = new CodeValidationIssue(diagnostics, issueSeverity, issueTypeCode); 779 CodeValidationIssueDetails issueDetails = new CodeValidationIssueDetails(details.getText()); 780 details.getCoding().forEach(coding -> issueDetails.addCoding(coding.getSystem(), coding.getCode())); 781 issue.setDetails(issueDetails); 782 return issue; 783 }) 784 .collect(Collectors.toList()); 785 } 786 787 private static CodeValidationIssue createCodeValidationIssue(String theMessage) { 788 return new CodeValidationIssue( 789 theMessage, 790 IssueSeverity.ERROR, 791 CodeValidationIssueCode.INVALID, 792 CodeValidationIssueCoding.INVALID_CODE); 793 } 794 795 public interface ValidationErrorMessageBuilder { 796 String buildErrorMessage(String theServerMessage); 797 } 798 799 protected IBaseParameters buildValidateCodeInputParameters( 800 String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, IBaseResource theValueSet) { 801 final FhirContext fhirContext = getFhirContext(); 802 IBaseParameters params = ParametersUtil.newInstance(fhirContext); 803 804 if (theValueSet == null && theValueSetUrl == null) { 805 ParametersUtil.addParameterToParametersUri(fhirContext, params, "url", theCodeSystem); 806 ParametersUtil.addParameterToParametersString(fhirContext, params, "code", theCode); 807 if (isNotBlank(theDisplay)) { 808 ParametersUtil.addParameterToParametersString(fhirContext, params, "display", theDisplay); 809 } 810 return params; 811 } 812 813 if (isNotBlank(theValueSetUrl)) { 814 ParametersUtil.addParameterToParametersUri(fhirContext, params, "url", theValueSetUrl); 815 } 816 ParametersUtil.addParameterToParametersString(fhirContext, params, "code", theCode); 817 if (isNotBlank(theCodeSystem)) { 818 ParametersUtil.addParameterToParametersUri(fhirContext, params, "system", theCodeSystem); 819 } 820 if (isNotBlank(theDisplay)) { 821 ParametersUtil.addParameterToParametersString(fhirContext, params, "display", theDisplay); 822 } 823 if (theValueSet != null) { 824 ParametersUtil.addParameterToParameters(fhirContext, params, "valueSet", theValueSet); 825 } 826 return params; 827 } 828 829 /** 830 * Sets the FHIR Terminology Server base URL 831 * 832 * @param theBaseUrl The base URL, e.g. "<a href="https://hapi.fhir.org/baseR4">...</a>" 833 */ 834 public void setBaseUrl(String theBaseUrl) { 835 Validate.notBlank(theBaseUrl, "theBaseUrl must be provided"); 836 myBaseUrl = theBaseUrl; 837 } 838 839 /** 840 * Adds an interceptor that will be registered to all clients. 841 * <p> 842 * Note that this method is not thread-safe and should only be called prior to this module 843 * being used. 844 * </p> 845 * 846 * @param theClientInterceptor The interceptor (must not be null) 847 */ 848 public void addClientInterceptor(@Nonnull Object theClientInterceptor) { 849 Validate.notNull(theClientInterceptor, "theClientInterceptor must not be null"); 850 myClientInterceptors.add(theClientInterceptor); 851 } 852}