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.gclient.IQuery; 015import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 016import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 017import ca.uhn.fhir.util.BundleUtil; 018import ca.uhn.fhir.util.ParametersUtil; 019import jakarta.annotation.Nonnull; 020import jakarta.annotation.Nullable; 021import org.apache.commons.lang3.StringUtils; 022import org.apache.commons.lang3.Validate; 023import org.hl7.fhir.instance.model.api.IBaseBundle; 024import org.hl7.fhir.instance.model.api.IBaseDatatype; 025import org.hl7.fhir.instance.model.api.IBaseParameters; 026import org.hl7.fhir.instance.model.api.IBaseResource; 027import org.hl7.fhir.r4.model.Base; 028import org.hl7.fhir.r4.model.CodeSystem; 029import org.hl7.fhir.r4.model.CodeType; 030import org.hl7.fhir.r4.model.Coding; 031import org.hl7.fhir.r4.model.Parameters; 032import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; 033import org.hl7.fhir.r4.model.Property; 034import org.hl7.fhir.r4.model.StringType; 035import org.hl7.fhir.r4.model.Type; 036import org.slf4j.Logger; 037import org.slf4j.LoggerFactory; 038 039import java.util.ArrayList; 040import java.util.List; 041import java.util.Objects; 042 043import static org.apache.commons.lang3.StringUtils.isBlank; 044import static org.apache.commons.lang3.StringUtils.isNotBlank; 045 046/** 047 * This class is an implementation of {@link IValidationSupport} that fetches validation codes 048 * from a remote FHIR based terminology server. It will invoke the FHIR 049 * <a href="http://hl7.org/fhir/valueset-operation-validate-code.html">ValueSet/$validate-code</a> 050 * operation in order to validate codes. 051 */ 052public class RemoteTerminologyServiceValidationSupport extends BaseValidationSupport implements IValidationSupport { 053 private static final Logger ourLog = LoggerFactory.getLogger(RemoteTerminologyServiceValidationSupport.class); 054 055 private String myBaseUrl; 056 private final List<Object> myClientInterceptors = new ArrayList<>(); 057 058 /** 059 * Constructor 060 * 061 * @param theFhirContext The FhirContext object to use 062 */ 063 public RemoteTerminologyServiceValidationSupport(FhirContext theFhirContext) { 064 super(theFhirContext); 065 } 066 067 public RemoteTerminologyServiceValidationSupport(FhirContext theFhirContext, String theBaseUrl) { 068 super(theFhirContext); 069 myBaseUrl = theBaseUrl; 070 } 071 072 @Override 073 public String getName() { 074 return getFhirContext().getVersion().getVersion() + " Remote Terminology Service Validation Support"; 075 } 076 077 @Override 078 public CodeValidationResult validateCode( 079 ValidationSupportContext theValidationSupportContext, 080 ConceptValidationOptions theOptions, 081 String theCodeSystem, 082 String theCode, 083 String theDisplay, 084 String theValueSetUrl) { 085 086 return invokeRemoteValidateCode(theCodeSystem, theCode, theDisplay, theValueSetUrl, null); 087 } 088 089 @Override 090 public CodeValidationResult validateCodeInValueSet( 091 ValidationSupportContext theValidationSupportContext, 092 ConceptValidationOptions theOptions, 093 String theCodeSystem, 094 String theCode, 095 String theDisplay, 096 @Nonnull IBaseResource theValueSet) { 097 098 IBaseResource valueSet = theValueSet; 099 100 // some external validators require the system when the code is passed 101 // so let's try to get it from the VS if is not present 102 String codeSystem = theCodeSystem; 103 if (isNotBlank(theCode) && isBlank(codeSystem)) { 104 codeSystem = ValidationSupportUtils.extractCodeSystemForCode(theValueSet, theCode); 105 } 106 107 String valueSetUrl = DefaultProfileValidationSupport.getConformanceResourceUrl(myCtx, valueSet); 108 if (isNotBlank(valueSetUrl)) { 109 valueSet = null; 110 } else { 111 valueSetUrl = null; 112 } 113 return invokeRemoteValidateCode(codeSystem, theCode, theDisplay, valueSetUrl, valueSet); 114 } 115 116 @Override 117 public IBaseResource fetchCodeSystem(String theSystem) { 118 // callers of this want the whole resource. 119 return fetchCodeSystem(theSystem, SummaryEnum.FALSE); 120 } 121 122 /** 123 * Fetch the code system, possibly a summary. 124 * @param theSystem the canonical url 125 * @param theSummaryParam to force a summary mode - or null to allow server default. 126 * @return the CodeSystem 127 */ 128 @Nullable 129 private IBaseResource fetchCodeSystem(String theSystem, @Nullable SummaryEnum theSummaryParam) { 130 IGenericClient client = provideClient(); 131 Class<? extends IBaseBundle> bundleType = 132 myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class); 133 IQuery<IBaseBundle> codeSystemQuery = client.search() 134 .forResource("CodeSystem") 135 .where(CodeSystem.URL.matches().value(theSystem)); 136 137 if (theSummaryParam != null) { 138 codeSystemQuery.summaryMode(theSummaryParam); 139 } 140 141 IBaseBundle results = codeSystemQuery.returnBundle(bundleType).execute(); 142 List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results); 143 if (!resultsList.isEmpty()) { 144 return resultsList.get(0); 145 } 146 147 return null; 148 } 149 150 @Override 151 public LookupCodeResult lookupCode( 152 ValidationSupportContext theValidationSupportContext, @Nonnull LookupCodeRequest theLookupCodeRequest) { 153 final String code = theLookupCodeRequest.getCode(); 154 final String system = theLookupCodeRequest.getSystem(); 155 final String displayLanguage = theLookupCodeRequest.getDisplayLanguage(); 156 Validate.notBlank(code, "theCode must be provided"); 157 158 IGenericClient client = provideClient(); 159 FhirContext fhirContext = client.getFhirContext(); 160 FhirVersionEnum fhirVersion = fhirContext.getVersion().getVersion(); 161 162 if (fhirVersion.isNewerThan(FhirVersionEnum.R4) || fhirVersion.isOlderThan(FhirVersionEnum.DSTU3)) { 163 throw new UnsupportedOperationException(Msg.code(710) + "Unsupported FHIR version '" 164 + fhirVersion.getFhirVersionString() + "'. Only DSTU3 and R4 are supported."); 165 } 166 167 IBaseParameters params = ParametersUtil.newInstance(fhirContext); 168 ParametersUtil.addParameterToParametersString(fhirContext, params, "code", code); 169 if (!StringUtils.isEmpty(system)) { 170 ParametersUtil.addParameterToParametersString(fhirContext, params, "system", system); 171 } 172 if (!StringUtils.isEmpty(displayLanguage)) { 173 ParametersUtil.addParameterToParametersString(fhirContext, params, "language", displayLanguage); 174 } 175 for (String propertyName : theLookupCodeRequest.getPropertyNames()) { 176 ParametersUtil.addParameterToParametersCode(fhirContext, params, "property", propertyName); 177 } 178 Class<? extends IBaseResource> codeSystemClass = 179 myCtx.getResourceDefinition("CodeSystem").getImplementingClass(); 180 IBaseParameters outcome; 181 try { 182 outcome = client.operation() 183 .onType(codeSystemClass) 184 .named("$lookup") 185 .withParameters(params) 186 .useHttpGet() 187 .execute(); 188 } catch (ResourceNotFoundException | InvalidRequestException e) { 189 // this can potentially be moved to an interceptor and be reused in other areas 190 // where we call a remote server or by the client as a custom interceptor 191 // that interceptor would alter the status code of the response and the body into a different format 192 // e.g. ClientResponseInterceptorModificationTemplate 193 ourLog.error(e.getMessage(), e); 194 LookupCodeResult result = LookupCodeResult.notFound(system, code); 195 result.setErrorMessage( 196 getErrorMessage("unknownCodeInSystem", system, code, client.getServerBase(), e.getMessage())); 197 return result; 198 } 199 if (outcome != null && !outcome.isEmpty()) { 200 if (fhirVersion == FhirVersionEnum.DSTU3) { 201 return generateLookupCodeResultDstu3(code, system, (org.hl7.fhir.dstu3.model.Parameters) outcome); 202 } 203 if (fhirVersion == FhirVersionEnum.R4) { 204 return generateLookupCodeResultR4(code, system, (Parameters) outcome); 205 } 206 } 207 return LookupCodeResult.notFound(system, code); 208 } 209 210 protected String getErrorMessage(String errorCode, Object... theParams) { 211 return getFhirContext().getLocalizer().getMessage(getClass(), errorCode, theParams); 212 } 213 214 private LookupCodeResult generateLookupCodeResultDstu3( 215 String theCode, String theSystem, org.hl7.fhir.dstu3.model.Parameters outcomeDSTU3) { 216 // NOTE: I wanted to put all of this logic into the IValidationSupport Class, but it would've required adding 217 // several new dependencies on version-specific libraries and that is explicitly forbidden (see comment in 218 // POM). 219 LookupCodeResult result = new LookupCodeResult(); 220 result.setSearchedForCode(theCode); 221 result.setSearchedForSystem(theSystem); 222 result.setFound(true); 223 for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent parameterComponent : 224 outcomeDSTU3.getParameter()) { 225 String parameterTypeAsString = Objects.toString(parameterComponent.getValue(), null); 226 switch (parameterComponent.getName()) { 227 case "property": 228 BaseConceptProperty conceptProperty = createConceptPropertyDstu3(parameterComponent); 229 if (conceptProperty != null) { 230 result.getProperties().add(conceptProperty); 231 } 232 break; 233 case "designation": 234 ConceptDesignation conceptDesignation = createConceptDesignationDstu3(parameterComponent); 235 result.getDesignations().add(conceptDesignation); 236 break; 237 case "name": 238 result.setCodeSystemDisplayName(parameterTypeAsString); 239 break; 240 case "version": 241 result.setCodeSystemVersion(parameterTypeAsString); 242 break; 243 case "display": 244 result.setCodeDisplay(parameterTypeAsString); 245 break; 246 case "abstract": 247 result.setCodeIsAbstract(Boolean.parseBoolean(parameterTypeAsString)); 248 break; 249 default: 250 } 251 } 252 return result; 253 } 254 255 private static BaseConceptProperty createConceptPropertyDstu3( 256 org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent theParameterComponent) { 257 org.hl7.fhir.dstu3.model.Property property = theParameterComponent.getChildByName("part"); 258 259 // The assumption here is that we may at east 2 elements in this part 260 if (property == null || property.getValues().size() < 2) { 261 return null; 262 } 263 264 List<org.hl7.fhir.dstu3.model.Base> values = property.getValues(); 265 org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent firstPart = 266 (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) values.get(0); 267 String propertyName = ((org.hl7.fhir.dstu3.model.CodeType) firstPart.getValue()).getValue(); 268 269 org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent secondPart = 270 (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) values.get(1); 271 org.hl7.fhir.dstu3.model.Type value = secondPart.getValue(); 272 273 if (value != null) { 274 return createConceptPropertyDstu3(propertyName, value); 275 } 276 277 String groupName = secondPart.getName(); 278 if (!"subproperty".equals(groupName)) { 279 return null; 280 } 281 282 // handle property group (a property containing sub-properties) 283 GroupConceptProperty groupConceptProperty = new GroupConceptProperty(propertyName); 284 285 // we already retrieved the property name (group name) as first element, next will be the sub-properties. 286 // there is no dedicated value for a property group as it is an aggregate 287 for (int i = 1; i < values.size(); i++) { 288 org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent nextPart = 289 (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) values.get(i); 290 BaseConceptProperty subProperty = createConceptPropertyDstu3(nextPart); 291 if (subProperty != null) { 292 groupConceptProperty.addSubProperty(subProperty); 293 } 294 } 295 return groupConceptProperty; 296 } 297 298 public static BaseConceptProperty createConceptProperty(final String theName, final IBaseDatatype theValue) { 299 if (theValue instanceof Type) { 300 return createConceptPropertyR4(theName, (Type) theValue); 301 } 302 if (theValue instanceof org.hl7.fhir.dstu3.model.Type) { 303 return createConceptPropertyDstu3(theName, (org.hl7.fhir.dstu3.model.Type) theValue); 304 } 305 return null; 306 } 307 308 private static BaseConceptProperty createConceptPropertyDstu3( 309 final String theName, final org.hl7.fhir.dstu3.model.Type theValue) { 310 if (theValue == null) { 311 return null; 312 } 313 BaseConceptProperty conceptProperty; 314 String fhirType = theValue.fhirType(); 315 switch (fhirType) { 316 case IValidationSupport.TYPE_STRING: 317 org.hl7.fhir.dstu3.model.StringType stringType = (org.hl7.fhir.dstu3.model.StringType) theValue; 318 conceptProperty = new StringConceptProperty(theName, stringType.getValue()); 319 break; 320 case IValidationSupport.TYPE_CODING: 321 org.hl7.fhir.dstu3.model.Coding coding = (org.hl7.fhir.dstu3.model.Coding) theValue; 322 conceptProperty = 323 new CodingConceptProperty(theName, coding.getSystem(), coding.getCode(), coding.getDisplay()); 324 break; 325 // TODO: add other property types as per FHIR spec https://github.com/hapifhir/hapi-fhir/issues/5699 326 default: 327 // other types will not fail for Remote Terminology 328 conceptProperty = new StringConceptProperty(theName, theValue.toString()); 329 } 330 return conceptProperty; 331 } 332 333 private ConceptDesignation createConceptDesignationDstu3( 334 org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent theParameterComponent) { 335 ConceptDesignation conceptDesignation = new ConceptDesignation(); 336 for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent designationComponent : 337 theParameterComponent.getPart()) { 338 org.hl7.fhir.dstu3.model.Type designationComponentValue = designationComponent.getValue(); 339 if (designationComponentValue == null) { 340 continue; 341 } 342 switch (designationComponent.getName()) { 343 case "language": 344 conceptDesignation.setLanguage(designationComponentValue.toString()); 345 break; 346 case "use": 347 org.hl7.fhir.dstu3.model.Coding coding = 348 (org.hl7.fhir.dstu3.model.Coding) designationComponentValue; 349 conceptDesignation.setUseSystem(coding.getSystem()); 350 conceptDesignation.setUseCode(coding.getCode()); 351 conceptDesignation.setUseDisplay(coding.getDisplay()); 352 break; 353 case "value": 354 conceptDesignation.setValue(designationComponent.getValue().toString()); 355 break; 356 default: 357 } 358 } 359 return conceptDesignation; 360 } 361 362 private LookupCodeResult generateLookupCodeResultR4(String theCode, String theSystem, Parameters outcomeR4) { 363 // NOTE: I wanted to put all of this logic into the IValidationSupport Class, but it would've required adding 364 // several new dependencies on version-specific libraries and that is explicitly forbidden (see comment in 365 // POM). 366 LookupCodeResult result = new LookupCodeResult(); 367 result.setSearchedForCode(theCode); 368 result.setSearchedForSystem(theSystem); 369 result.setFound(true); 370 for (ParametersParameterComponent parameterComponent : outcomeR4.getParameter()) { 371 String parameterTypeAsString = Objects.toString(parameterComponent.getValue(), null); 372 switch (parameterComponent.getName()) { 373 case "property": 374 BaseConceptProperty conceptProperty = createConceptPropertyR4(parameterComponent); 375 if (conceptProperty != null) { 376 result.getProperties().add(conceptProperty); 377 } 378 break; 379 case "designation": 380 ConceptDesignation conceptDesignation = createConceptDesignationR4(parameterComponent); 381 result.getDesignations().add(conceptDesignation); 382 break; 383 case "name": 384 result.setCodeSystemDisplayName(parameterTypeAsString); 385 break; 386 case "version": 387 result.setCodeSystemVersion(parameterTypeAsString); 388 break; 389 case "display": 390 result.setCodeDisplay(parameterTypeAsString); 391 break; 392 case "abstract": 393 result.setCodeIsAbstract(Boolean.parseBoolean(parameterTypeAsString)); 394 break; 395 default: 396 } 397 } 398 return result; 399 } 400 401 private static BaseConceptProperty createConceptPropertyR4(ParametersParameterComponent thePropertyComponent) { 402 Property property = thePropertyComponent.getChildByName("part"); 403 404 // The assumption here is that we may at east 2 elements in this part 405 if (property == null || property.getValues().size() < 2) { 406 return null; 407 } 408 409 List<Base> values = property.getValues(); 410 ParametersParameterComponent firstPart = (ParametersParameterComponent) values.get(0); 411 String propertyName = ((CodeType) firstPart.getValue()).getValue(); 412 413 ParametersParameterComponent secondPart = (ParametersParameterComponent) values.get(1); 414 Type value = secondPart.getValue(); 415 416 if (value != null) { 417 return createConceptPropertyR4(propertyName, value); 418 } 419 420 String groupName = secondPart.getName(); 421 if (!"subproperty".equals(groupName)) { 422 return null; 423 } 424 425 // handle property group (a property containing sub-properties) 426 GroupConceptProperty groupConceptProperty = new GroupConceptProperty(propertyName); 427 428 // we already retrieved the property name (group name) as first element, next will be the sub-properties. 429 // there is no dedicated value for a property group as it is an aggregate 430 for (int i = 1; i < values.size(); i++) { 431 ParametersParameterComponent nextPart = (ParametersParameterComponent) values.get(i); 432 BaseConceptProperty subProperty = createConceptPropertyR4(nextPart); 433 if (subProperty != null) { 434 groupConceptProperty.addSubProperty(subProperty); 435 } 436 } 437 return groupConceptProperty; 438 } 439 440 private static BaseConceptProperty createConceptPropertyR4(final String theName, final Type theValue) { 441 BaseConceptProperty conceptProperty; 442 443 String fhirType = theValue.fhirType(); 444 switch (fhirType) { 445 case IValidationSupport.TYPE_STRING: 446 StringType stringType = (StringType) theValue; 447 conceptProperty = new StringConceptProperty(theName, stringType.getValue()); 448 break; 449 case IValidationSupport.TYPE_CODING: 450 Coding coding = (Coding) theValue; 451 conceptProperty = 452 new CodingConceptProperty(theName, coding.getSystem(), coding.getCode(), coding.getDisplay()); 453 break; 454 // TODO: add other property types as per FHIR spec https://github.com/hapifhir/hapi-fhir/issues/5699 455 default: 456 // other types will not fail for Remote Terminology 457 conceptProperty = new StringConceptProperty(theName, theValue.toString()); 458 } 459 return conceptProperty; 460 } 461 462 private ConceptDesignation createConceptDesignationR4(ParametersParameterComponent theParameterComponent) { 463 ConceptDesignation conceptDesignation = new ConceptDesignation(); 464 for (ParametersParameterComponent designationComponent : theParameterComponent.getPart()) { 465 Type designationComponentValue = designationComponent.getValue(); 466 if (designationComponentValue == null) { 467 continue; 468 } 469 switch (designationComponent.getName()) { 470 case "language": 471 conceptDesignation.setLanguage(designationComponentValue.toString()); 472 break; 473 case "use": 474 Coding coding = (Coding) designationComponentValue; 475 conceptDesignation.setUseSystem(coding.getSystem()); 476 conceptDesignation.setUseCode(coding.getCode()); 477 conceptDesignation.setUseDisplay(coding.getDisplay()); 478 break; 479 case "value": 480 conceptDesignation.setValue(designationComponentValue.toString()); 481 break; 482 default: 483 } 484 } 485 return conceptDesignation; 486 } 487 488 @Override 489 public IBaseResource fetchValueSet(String theValueSetUrl) { 490 // force the remote server to send the whole resource. 491 SummaryEnum summaryParam = SummaryEnum.FALSE; 492 return fetchValueSet(theValueSetUrl, summaryParam); 493 } 494 495 /** 496 * Search for a ValueSet by canonical url via IGenericClient. 497 * 498 * @param theValueSetUrl the canonical url of the ValueSet 499 * @param theSummaryParam force a summary mode - null allows server default 500 * @return the ValueSet or null if none match the url 501 */ 502 @Nullable 503 private IBaseResource fetchValueSet(String theValueSetUrl, SummaryEnum theSummaryParam) { 504 IGenericClient client = provideClient(); 505 Class<? extends IBaseBundle> bundleType = 506 myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class); 507 508 IQuery<IBaseBundle> valueSetQuery = client.search() 509 .forResource("ValueSet") 510 .where(CodeSystem.URL.matches().value(theValueSetUrl)); 511 512 if (theSummaryParam != null) { 513 valueSetQuery.summaryMode(theSummaryParam); 514 } 515 516 IBaseBundle results = valueSetQuery.returnBundle(bundleType).execute(); 517 518 List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results); 519 if (!resultsList.isEmpty()) { 520 return resultsList.get(0); 521 } 522 523 return null; 524 } 525 526 @Override 527 public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { 528 // a summary is ok if we are just checking the presence. 529 SummaryEnum summaryParam = null; 530 531 return fetchCodeSystem(theSystem, summaryParam) != null; 532 } 533 534 @Override 535 public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) { 536 // a summary is ok if we are just checking the presence. 537 SummaryEnum summaryParam = null; 538 539 return fetchValueSet(theValueSetUrl, summaryParam) != null; 540 } 541 542 @Override 543 public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) { 544 IGenericClient client = provideClient(); 545 FhirContext fhirContext = client.getFhirContext(); 546 547 IBaseParameters params = RemoteTerminologyUtil.buildTranslateInputParameters(fhirContext, theRequest); 548 549 IBaseParameters outcome = client.operation() 550 .onType("ConceptMap") 551 .named("$translate") 552 .withParameters(params) 553 .execute(); 554 555 return RemoteTerminologyUtil.translateOutcomeToResults(fhirContext, outcome); 556 } 557 558 private IGenericClient provideClient() { 559 IGenericClient retVal = myCtx.newRestfulGenericClient(myBaseUrl); 560 for (Object next : myClientInterceptors) { 561 retVal.registerInterceptor(next); 562 } 563 return retVal; 564 } 565 566 public String getBaseUrl() { 567 return myBaseUrl; 568 } 569 570 protected CodeValidationResult invokeRemoteValidateCode( 571 String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, IBaseResource theValueSet) { 572 if (isBlank(theCode)) { 573 return null; 574 } 575 576 IGenericClient client = provideClient(); 577 578 IBaseParameters input = 579 buildValidateCodeInputParameters(theCodeSystem, theCode, theDisplay, theValueSetUrl, theValueSet); 580 581 String resourceType = "ValueSet"; 582 if (theValueSet == null && theValueSetUrl == null) { 583 resourceType = "CodeSystem"; 584 } 585 586 IBaseParameters output; 587 try { 588 output = client.operation() 589 .onType(resourceType) 590 .named("validate-code") 591 .withParameters(input) 592 .execute(); 593 } catch (ResourceNotFoundException | InvalidRequestException ex) { 594 ourLog.error(ex.getMessage(), ex); 595 CodeValidationResult result = new CodeValidationResult(); 596 result.setSeverity(IssueSeverity.ERROR); 597 String errorMessage = buildErrorMessage( 598 theCodeSystem, theCode, theValueSetUrl, theValueSet, client.getServerBase(), ex.getMessage()); 599 result.setMessage(errorMessage); 600 return result; 601 } 602 603 List<String> resultValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "result"); 604 if (resultValues.isEmpty() || isBlank(resultValues.get(0))) { 605 return null; 606 } 607 Validate.isTrue(resultValues.size() == 1, "Response contained %d 'result' values", resultValues.size()); 608 609 boolean success = "true".equalsIgnoreCase(resultValues.get(0)); 610 611 CodeValidationResult retVal = new CodeValidationResult(); 612 if (success) { 613 614 retVal.setCode(theCode); 615 List<String> displayValues = 616 ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "display"); 617 if (!displayValues.isEmpty()) { 618 retVal.setDisplay(displayValues.get(0)); 619 } 620 621 } else { 622 623 retVal.setSeverity(IssueSeverity.ERROR); 624 List<String> messageValues = 625 ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "message"); 626 if (!messageValues.isEmpty()) { 627 retVal.setMessage(messageValues.get(0)); 628 } 629 } 630 return retVal; 631 } 632 633 private String buildErrorMessage( 634 String theCodeSystem, 635 String theCode, 636 String theValueSetUrl, 637 IBaseResource theValueSet, 638 String theServerUrl, 639 String theServerMessage) { 640 if (theValueSetUrl == null && theValueSet == null) { 641 return getErrorMessage("unknownCodeInSystem", theCodeSystem, theCode, theServerUrl, theServerMessage); 642 } else { 643 return getErrorMessage( 644 "unknownCodeInValueSet", theCodeSystem, theCode, theValueSetUrl, theServerUrl, theServerMessage); 645 } 646 } 647 648 protected IBaseParameters buildValidateCodeInputParameters( 649 String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, IBaseResource theValueSet) { 650 IBaseParameters params = ParametersUtil.newInstance(getFhirContext()); 651 652 if (theValueSet == null && theValueSetUrl == null) { 653 ParametersUtil.addParameterToParametersUri(getFhirContext(), params, "url", theCodeSystem); 654 ParametersUtil.addParameterToParametersString(getFhirContext(), params, "code", theCode); 655 if (isNotBlank(theDisplay)) { 656 ParametersUtil.addParameterToParametersString(getFhirContext(), params, "display", theDisplay); 657 } 658 return params; 659 } 660 661 if (isNotBlank(theValueSetUrl)) { 662 ParametersUtil.addParameterToParametersUri(getFhirContext(), params, "url", theValueSetUrl); 663 } 664 ParametersUtil.addParameterToParametersString(getFhirContext(), params, "code", theCode); 665 if (isNotBlank(theCodeSystem)) { 666 ParametersUtil.addParameterToParametersUri(getFhirContext(), params, "system", theCodeSystem); 667 } 668 if (isNotBlank(theDisplay)) { 669 ParametersUtil.addParameterToParametersString(getFhirContext(), params, "display", theDisplay); 670 } 671 if (theValueSet != null) { 672 ParametersUtil.addParameterToParameters(getFhirContext(), params, "valueSet", theValueSet); 673 } 674 return params; 675 } 676 677 /** 678 * Sets the FHIR Terminology Server base URL 679 * 680 * @param theBaseUrl The base URL, e.g. "<a href="https://hapi.fhir.org/baseR4">...</a>" 681 */ 682 public void setBaseUrl(String theBaseUrl) { 683 Validate.notBlank(theBaseUrl, "theBaseUrl must be provided"); 684 myBaseUrl = theBaseUrl; 685 } 686 687 /** 688 * Adds an interceptor that will be registered to all clients. 689 * <p> 690 * Note that this method is not thread-safe and should only be called prior to this module 691 * being used. 692 * </p> 693 * 694 * @param theClientInterceptor The interceptor (must not be null) 695 */ 696 public void addClientInterceptor(@Nonnull Object theClientInterceptor) { 697 Validate.notNull(theClientInterceptor, "theClientInterceptor must not be null"); 698 myClientInterceptors.add(theClientInterceptor); 699 } 700}