001package org.hl7.fhir.r4b.terminologies; 002 003/* 004 Copyright (c) 2011+, HL7, Inc. 005 All rights reserved. 006 007 Redistribution and use in source and binary forms, with or without modification, 008 are permitted provided that the following conditions are met: 009 010 * Redistributions of source code must retain the above copyright notice, this 011 list of conditions and the following disclaimer. 012 * Redistributions in binary form must reproduce the above copyright notice, 013 this list of conditions and the following disclaimer in the documentation 014 and/or other materials provided with the distribution. 015 * Neither the name of HL7 nor the names of its contributors may be used to 016 endorse or promote products derived from this software without specific 017 prior written permission. 018 019 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 022 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 023 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 024 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 025 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 026 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 027 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 028 POSSIBILITY OF SUCH DAMAGE. 029 030 */ 031 032 033 034import java.util.ArrayList; 035import java.util.HashMap; 036import java.util.List; 037import java.util.Map; 038 039import org.hl7.fhir.exceptions.FHIRException; 040import org.hl7.fhir.exceptions.NoTerminologyServiceException; 041import org.hl7.fhir.r4b.context.IWorkerContext; 042import org.hl7.fhir.r4b.context.IWorkerContext.ValidationResult; 043import org.hl7.fhir.r4b.model.CanonicalType; 044import org.hl7.fhir.r4b.model.CodeSystem; 045import org.hl7.fhir.r4b.model.CodeSystem.CodeSystemContentMode; 046import org.hl7.fhir.r4b.model.CodeSystem.ConceptDefinitionComponent; 047import org.hl7.fhir.r4b.model.CodeSystem.ConceptDefinitionDesignationComponent; 048import org.hl7.fhir.r4b.model.CodeableConcept; 049import org.hl7.fhir.r4b.model.Coding; 050import org.hl7.fhir.r4b.model.Enumerations.PublicationStatus; 051import org.hl7.fhir.r4b.model.UriType; 052import org.hl7.fhir.r4b.model.ValueSet; 053import org.hl7.fhir.r4b.model.ValueSet.ConceptReferenceComponent; 054import org.hl7.fhir.r4b.model.ValueSet.ConceptReferenceDesignationComponent; 055import org.hl7.fhir.r4b.model.ValueSet.ConceptSetComponent; 056import org.hl7.fhir.r4b.model.ValueSet.ConceptSetFilterComponent; 057import org.hl7.fhir.r4b.model.ValueSet.ValueSetExpansionContainsComponent; 058import org.hl7.fhir.r4b.terminologies.ValueSetExpander.TerminologyServiceErrorClass; 059import org.hl7.fhir.r4b.utils.ToolingExtensions; 060import org.hl7.fhir.r4b.utils.validation.ValidationContextCarrier; 061import org.hl7.fhir.r4b.utils.validation.ValidationContextCarrier.ValidationContextResourceProxy; 062import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 063import org.hl7.fhir.utilities.Utilities; 064import org.hl7.fhir.utilities.i18n.I18nConstants; 065import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 066import org.hl7.fhir.utilities.validation.ValidationOptions; 067import org.hl7.fhir.utilities.validation.ValidationOptions.ValueSetMode; 068 069public class ValueSetCheckerSimple extends ValueSetWorker implements ValueSetChecker { 070 071 private ValueSet valueset; 072 private IWorkerContext context; 073 private Map<String, ValueSetCheckerSimple> inner = new HashMap<>(); 074 private ValidationOptions options; 075 private ValidationContextCarrier localContext; 076 private List<CodeSystem> localSystems = new ArrayList<>(); 077 078 public ValueSetCheckerSimple(ValidationOptions options, ValueSet source, IWorkerContext context) { 079 this.valueset = source; 080 this.context = context; 081 this.options = options; 082 } 083 084 public ValueSetCheckerSimple(ValidationOptions options, ValueSet source, IWorkerContext context, ValidationContextCarrier ctxt) { 085 this.valueset = source; 086 this.context = context; 087 this.options = options; 088 this.localContext = ctxt; 089 analyseValueSet(); 090 } 091 092 private void analyseValueSet() { 093 if (localContext != null) { 094 if (valueset != null) { 095 for (ConceptSetComponent i : valueset.getCompose().getInclude()) { 096 analyseComponent(i); 097 } 098 for (ConceptSetComponent i : valueset.getCompose().getExclude()) { 099 analyseComponent(i); 100 } 101 } 102 } 103 } 104 105 private void analyseComponent(ConceptSetComponent i) { 106 if (i.getSystemElement().hasExtension(ToolingExtensions.EXT_VALUESET_SYSTEM)) { 107 String ref = i.getSystemElement().getExtensionString(ToolingExtensions.EXT_VALUESET_SYSTEM); 108 if (ref.startsWith("#")) { 109 String id = ref.substring(1); 110 for (ValidationContextResourceProxy t : localContext.getResources()) { 111 CodeSystem cs = (CodeSystem) t.loadContainedResource(id, CodeSystem.class); 112 if (cs != null) { 113 localSystems.add(cs); 114 } 115 } 116 } else { 117 throw new Error("Not done yet #2: "+ref); 118 } 119 } 120 } 121 122 public ValidationResult validateCode(CodeableConcept code) throws FHIRException { 123 // first, we validate the codings themselves 124 List<String> errors = new ArrayList<String>(); 125 List<String> warnings = new ArrayList<String>(); 126 if (options.getValueSetMode() != ValueSetMode.CHECK_MEMERSHIP_ONLY) { 127 for (Coding c : code.getCoding()) { 128 if (!c.hasSystem()) { 129 warnings.add(context.formatMessage(I18nConstants.CODING_HAS_NO_SYSTEM__CANNOT_VALIDATE)); 130 } 131 CodeSystem cs = resolveCodeSystem(c.getSystem()); 132 ValidationResult res = null; 133 if (cs == null || cs.getContent() != CodeSystemContentMode.COMPLETE) { 134 res = context.validateCode(options.noClient(), c, null); 135 } else { 136 res = validateCode(c, cs); 137 } 138 if (!res.isOk()) { 139 errors.add(res.getMessage()); 140 } else if (res.getMessage() != null) { 141 warnings.add(res.getMessage()); 142 } 143 } 144 } 145 if (valueset != null && options.getValueSetMode() != ValueSetMode.NO_MEMBERSHIP_CHECK) { 146 Boolean result = false; 147 for (Coding c : code.getCoding()) { 148 Boolean ok = codeInValueSet(c.getSystem(), c.getCode(), warnings); 149 if (ok == null && result == false) { 150 result = null; 151 } else if (ok) { 152 result = true; 153 } 154 } 155 if (result == null) { 156 warnings.add(0, context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl())); 157 } else if (!result) { 158 errors.add(0, context.formatMessage(I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl())); 159 } 160 } 161 if (errors.size() > 0) { 162 return new ValidationResult(IssueSeverity.ERROR, errors.toString()); 163 } else if (warnings.size() > 0) { 164 return new ValidationResult(IssueSeverity.WARNING, warnings.toString()); 165 } else { 166 return new ValidationResult(IssueSeverity.INFORMATION, null); 167 } 168 } 169 170 public CodeSystem resolveCodeSystem(String system) { 171 for (CodeSystem t : localSystems) { 172 if (t.getUrl().equals(system)) { 173 return t; 174 } 175 } 176 CodeSystem cs = context.fetchCodeSystem(system); 177 if (cs == null) { 178 cs = findSpecialCodeSystem(system); 179 } 180 return cs; 181 } 182 183 public ValidationResult validateCode(Coding code) throws FHIRException { 184 String warningMessage = null; 185 // first, we validate the concept itself 186 187 ValidationResult res = null; 188 boolean inExpansion = false; 189 boolean inInclude = false; 190 String system = code.hasSystem() ? code.getSystem() : getValueSetSystemOrNull(); 191 if (options.getValueSetMode() != ValueSetMode.CHECK_MEMERSHIP_ONLY) { 192 if (system == null && !code.hasDisplay()) { // dealing with just a plain code (enum) 193 system = systemForCodeInValueSet(code.getCode()); 194 } 195 if (!code.hasSystem()) { 196 if (options.isGuessSystem() && system == null && Utilities.isAbsoluteUrl(code.getCode())) { 197 system = "urn:ietf:rfc:3986"; // this arises when using URIs bound to value sets 198 } 199 code.setSystem(system); 200 } 201 inExpansion = checkExpansion(code); 202 inInclude = checkInclude(code); 203 CodeSystem cs = resolveCodeSystem(system); 204 if (cs == null) { 205 warningMessage = "Unable to resolve system "+system; 206 if (!inExpansion) { 207 if (valueset != null && valueset.hasExpansion()) { 208 return new ValidationResult(IssueSeverity.ERROR, context.formatMessage(I18nConstants.CODESYSTEM_CS_UNK_EXPANSION, valueset.getUrl(), code.getCode().toString(), code.getSystem())); 209 } else { 210 throw new FHIRException(warningMessage); 211 } 212 } 213 } 214 if (cs != null && cs.hasSupplements()) { 215 return new ValidationResult(IssueSeverity.ERROR, context.formatMessage(I18nConstants.CODESYSTEM_CS_NO_SUPPLEMENT, cs.getUrl())); 216 } 217 if (cs!=null && cs.getContent() != CodeSystemContentMode.COMPLETE) { 218 warningMessage = "Resolved system "+system+", but the definition is not complete"; 219 if (!inExpansion && cs.getContent() != CodeSystemContentMode.FRAGMENT) { // we're going to give it a go if it's a fragment 220 throw new FHIRException(warningMessage); 221 } 222 } 223 224 if (cs != null /*&& (cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT)*/) { 225 if (!(cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT)) { 226 // we can't validate that here. 227 throw new FHIRException("Unable to evaluate based on empty code system"); 228 } 229 res = validateCode(code, cs); 230 } else if (cs == null && valueset.hasExpansion() && inExpansion) { 231 // we just take the value set as face value then 232 res = new ValidationResult(system, new ConceptDefinitionComponent().setCode(code.getCode()).setDisplay(code.getDisplay())); 233 } else { 234 // well, we didn't find a code system - try the expansion? 235 // disabled waiting for discussion 236 throw new FHIRException("No try the server"); 237 } 238 } else { 239 inExpansion = checkExpansion(code); 240 inInclude = checkInclude(code); 241 } 242 243 List<String> warnings = new ArrayList<>(); 244 245 // then, if we have a value set, we check it's in the value set 246 if (valueset != null && options.getValueSetMode() != ValueSetMode.NO_MEMBERSHIP_CHECK) { 247 if ((res==null || res.isOk())) { 248 Boolean ok = codeInValueSet(system, code.getCode(), warnings); 249 if (ok == null || !ok) { 250 if (res == null) { 251 res = new ValidationResult((IssueSeverity) null, null); 252 } 253 if (!inExpansion && !inInclude) { 254 res.setMessage("Not in value set "+valueset.getUrl()).setSeverity(IssueSeverity.ERROR); 255 } else if (warningMessage!=null) { 256 res = new ValidationResult(IssueSeverity.WARNING, context.formatMessage(I18nConstants.CODE_FOUND_IN_EXPANSION_HOWEVER_, warningMessage)); 257 } else if (inExpansion) { 258 res.setMessage("Code found in expansion, however: " + res.getMessage()); 259 } else if (inInclude) { 260 res.setMessage("Code found in include, however: " + res.getMessage()); 261 } 262 } 263 } 264 } 265 return res; 266 } 267 268 private boolean checkInclude(Coding code) { 269 if (valueset == null || code.getSystem() == null || code.getCode() == null) { 270 return false; 271 } 272 for (ConceptSetComponent inc : valueset.getCompose().getExclude()) { 273 if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) { 274 for (ConceptReferenceComponent cc : inc.getConcept()) { 275 if (cc.hasCode() && cc.getCode().equals(code.getCode())) { 276 return false; 277 } 278 } 279 } 280 } 281 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 282 if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) { 283 for (ConceptReferenceComponent cc : inc.getConcept()) { 284 if (cc.hasCode() && cc.getCode().equals(code.getCode())) { 285 return true; 286 } 287 } 288 } 289 } 290 return false; 291 } 292 293 private CodeSystem findSpecialCodeSystem(String system) { 294 if ("urn:ietf:rfc:3986".equals(system)) { 295 CodeSystem cs = new CodeSystem(); 296 cs.setUrl(system); 297 cs.setUserData("tx.cs.special", new URICodeSystem()); 298 cs.setContent(CodeSystemContentMode.COMPLETE); 299 return cs; 300 } 301 return null; 302 } 303 304 private ValidationResult findCodeInExpansion(Coding code) { 305 if (valueset==null || !valueset.hasExpansion()) 306 return null; 307 return findCodeInExpansion(code, valueset.getExpansion().getContains()); 308 } 309 310 private ValidationResult findCodeInExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) { 311 for (ValueSetExpansionContainsComponent containsComponent: contains) { 312 if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) { 313 ConceptDefinitionComponent ccd = new ConceptDefinitionComponent(); 314 ccd.setCode(containsComponent.getCode()); 315 ccd.setDisplay(containsComponent.getDisplay()); 316 ValidationResult res = new ValidationResult(code.getSystem(), ccd); 317 return res; 318 } 319 if (containsComponent.hasContains()) { 320 ValidationResult res = findCodeInExpansion(code, containsComponent.getContains()); 321 if (res != null) { 322 return res; 323 } 324 } 325 } 326 return null; 327 } 328 329 private boolean checkExpansion(Coding code) { 330 if (valueset==null || !valueset.hasExpansion()) { 331 return false; 332 } 333 return checkExpansion(code, valueset.getExpansion().getContains()); 334 } 335 336 private boolean checkExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) { 337 for (ValueSetExpansionContainsComponent containsComponent: contains) { 338 if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) { 339 return true; 340 } 341 if (containsComponent.hasContains() && checkExpansion(code, containsComponent.getContains())) { 342 return true; 343 } 344 } 345 return false; 346 } 347 348 private ValidationResult validateCode(Coding code, CodeSystem cs) { 349 ConceptDefinitionComponent cc = cs.hasUserData("tx.cs.special") ? ((SpecialCodeSystem) cs.getUserData("tx.cs.special")).findConcept(code) : findCodeInConcept(cs.getConcept(), code.getCode()); 350 if (cc == null) { 351 if (cs.getContent() == CodeSystemContentMode.FRAGMENT) { 352 return new ValidationResult(IssueSeverity.WARNING, context.formatMessage(I18nConstants.UNKNOWN_CODE__IN_FRAGMENT, gen(code), cs.getUrl())); 353 } else { 354 return new ValidationResult(IssueSeverity.ERROR, context.formatMessage(I18nConstants.UNKNOWN_CODE__IN_, gen(code), cs.getUrl())); 355 } 356 } 357 if (code.getDisplay() == null) { 358 return new ValidationResult(code.getSystem(), cc); 359 } 360 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 361 if (cc.hasDisplay()) { 362 b.append(cc.getDisplay()); 363 if (code.getDisplay().equalsIgnoreCase(cc.getDisplay())) { 364 return new ValidationResult(code.getSystem(), cc); 365 } 366 } 367 for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) { 368 b.append(ds.getValue()); 369 if (code.getDisplay().equalsIgnoreCase(ds.getValue())) { 370 return new ValidationResult(code.getSystem(), cc); 371 } 372 } 373 // also check to see if the value set has another display 374 ConceptReferenceComponent vs = findValueSetRef(code.getSystem(), code.getCode()); 375 if (vs != null && (vs.hasDisplay() ||vs.hasDesignation())) { 376 if (vs.hasDisplay()) { 377 b.append(vs.getDisplay()); 378 if (code.getDisplay().equalsIgnoreCase(vs.getDisplay())) { 379 return new ValidationResult(code.getSystem(), cc); 380 } 381 } 382 for (ConceptReferenceDesignationComponent ds : vs.getDesignation()) { 383 b.append(ds.getValue()); 384 if (code.getDisplay().equalsIgnoreCase(ds.getValue())) { 385 return new ValidationResult(code.getSystem(), cc); 386 } 387 } 388 } 389 return new ValidationResult(IssueSeverity.WARNING, context.formatMessage(I18nConstants.DISPLAY_NAME_FOR__SHOULD_BE_ONE_OF__INSTEAD_OF_, code.getSystem(), code.getCode(), b.toString(), code.getDisplay()), code.getSystem(), cc); 390 } 391 392 private ConceptReferenceComponent findValueSetRef(String system, String code) { 393 if (valueset == null) 394 return null; 395 // if it has an expansion 396 for (ValueSetExpansionContainsComponent exp : valueset.getExpansion().getContains()) { 397 if (system.equals(exp.getSystem()) && code.equals(exp.getCode())) { 398 ConceptReferenceComponent cc = new ConceptReferenceComponent(); 399 cc.setDisplay(exp.getDisplay()); 400 cc.setDesignation(exp.getDesignation()); 401 return cc; 402 } 403 } 404 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 405 if (system.equals(inc.getSystem())) { 406 for (ConceptReferenceComponent cc : inc.getConcept()) { 407 if (cc.getCode().equals(code)) { 408 return cc; 409 } 410 } 411 } 412 for (CanonicalType url : inc.getValueSet()) { 413 ConceptReferenceComponent cc = getVs(url.asStringValue()).findValueSetRef(system, code); 414 if (cc != null) { 415 return cc; 416 } 417 } 418 } 419 return null; 420 } 421 422 private String gen(Coding code) { 423 if (code.hasSystem()) { 424 return code.getSystem()+"#"+code.getCode(); 425 } else { 426 return null; 427 } 428 } 429 430 private String getValueSetSystem() throws FHIRException { 431 if (valueset == null) { 432 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__NO_VALUE_SET)); 433 } 434 if (valueset.getCompose().getInclude().size() == 0) { 435 if (!valueset.hasExpansion() || valueset.getExpansion().getContains().size() == 0) { 436 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_NO_INCLUDES_OR_EXPANSION)); 437 } else { 438 String cs = valueset.getExpansion().getContains().get(0).getSystem(); 439 if (cs != null && checkSystem(valueset.getExpansion().getContains(), cs)) { 440 return cs; 441 } else { 442 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_EXPANSION_HAS_MULTIPLE_SYSTEMS)); 443 } 444 } 445 } 446 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 447 if (inc.hasValueSet()) { 448 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_IMPORTS)); 449 } 450 if (!inc.hasSystem()) { 451 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_NO_SYSTEM)); 452 } 453 } 454 if (valueset.getCompose().getInclude().size() == 1) { 455 return valueset.getCompose().getInclude().get(0).getSystem(); 456 } 457 458 return null; 459 } 460 461 private String getValueSetSystemOrNull() throws FHIRException { 462 if (valueset == null) { 463 return null; 464 } 465 if (valueset.getCompose().getInclude().size() == 0) { 466 if (!valueset.hasExpansion() || valueset.getExpansion().getContains().size() == 0) { 467 return null; 468 } else { 469 String cs = valueset.getExpansion().getContains().get(0).getSystem(); 470 if (cs != null && checkSystem(valueset.getExpansion().getContains(), cs)) { 471 return cs; 472 } else { 473 return null; 474 } 475 } 476 } 477 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 478 if (inc.hasValueSet()) { 479 return null; 480 } 481 if (!inc.hasSystem()) { 482 return null; 483 } 484 } 485 if (valueset.getCompose().getInclude().size() == 1) { 486 return valueset.getCompose().getInclude().get(0).getSystem(); 487 } 488 489 return null; 490 } 491 492 /* 493 * Check that all system values within an expansion correspond to the specified system value 494 */ 495 private boolean checkSystem(List<ValueSetExpansionContainsComponent> containsList, String system) { 496 for (ValueSetExpansionContainsComponent contains : containsList) { 497 if (!contains.getSystem().equals(system) || (contains.hasContains() && !checkSystem(contains.getContains(), system))) { 498 return false; 499 } 500 } 501 return true; 502 } 503 504 private ConceptDefinitionComponent findCodeInConcept(ConceptDefinitionComponent concept, String code) { 505 if (code.equals(concept.getCode())) { 506 return concept; 507 } 508 ConceptDefinitionComponent cc = findCodeInConcept(concept.getConcept(), code); 509 if (cc != null) { 510 return cc; 511 } 512 if (concept.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) { 513 List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) concept.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK); 514 for (ConceptDefinitionComponent c : children) { 515 cc = findCodeInConcept(c, code); 516 if (cc != null) { 517 return cc; 518 } 519 } 520 } 521 return null; 522 } 523 524 private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code) { 525 for (ConceptDefinitionComponent cc : concept) { 526 if (code.equals(cc.getCode())) { 527 return cc; 528 } 529 ConceptDefinitionComponent c = findCodeInConcept(cc, code); 530 if (c != null) { 531 return c; 532 } 533 } 534 return null; 535 } 536 537 538 private String systemForCodeInValueSet(String code) { 539 String sys = null; 540 if (valueset.hasCompose()) { 541 if (valueset.getCompose().hasExclude()) { 542 return null; 543 } 544 for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) { 545 if (vsi.hasValueSet()) { 546 return null; 547 } 548 if (!vsi.hasSystem()) { 549 return null; 550 } 551 if (vsi.hasFilter()) { 552 return null; 553 } 554 CodeSystem cs = resolveCodeSystem(vsi.getSystem()); 555 if (cs == null) { 556 return null; 557 } 558 if (vsi.hasConcept()) { 559 for (ConceptReferenceComponent cc : vsi.getConcept()) { 560 boolean match = cs.getCaseSensitive() ? cc.getCode().equals(code) : cc.getCode().equalsIgnoreCase(code); 561 if (match) { 562 if (sys == null) { 563 sys = vsi.getSystem(); 564 } else if (!sys.equals(vsi.getSystem())) { 565 return null; 566 } 567 } 568 } 569 } else { 570 ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code); 571 if (cc != null) { 572 if (sys == null) { 573 sys = vsi.getSystem(); 574 } else if (!sys.equals(vsi.getSystem())) { 575 return null; 576 } 577 } 578 } 579 } 580 } else if (valueset.hasExpansion()) { 581 // Retrieve a list of all systems associated with this code in the expansion 582 List<String> systems = new ArrayList<String>(); 583 checkSystems(valueset.getExpansion().getContains(), code, systems); 584 if (systems.size()==1) 585 sys = systems.get(0); 586 } 587 588 return sys; 589 } 590 591 /* 592 * Recursively go through all codes in the expansion and for any coding that matches the specified code, add the system for that coding 593 * to the passed list. 594 */ 595 private void checkSystems(List<ValueSetExpansionContainsComponent> contains, String code, List<String> systems) { 596 for (ValueSetExpansionContainsComponent c: contains) { 597 if (c.getCode().equals(code)) { 598 if (!systems.contains(c.getSystem())) 599 systems.add(c.getSystem()); 600 } 601 if (c.hasContains()) 602 checkSystems(c.getContains(), code, systems); 603 } 604 } 605 606 @Override 607 public Boolean codeInValueSet(String system, String code, List<String> warnings) throws FHIRException { 608 if (valueset == null) { 609 return false; 610 } 611 Boolean result = false; 612 613 if (valueset.hasExpansion()) { 614 return checkExpansion(new Coding(system, code, null)); 615 } else if (valueset.hasCompose()) { 616 int i = 0; 617 for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) { 618 Boolean ok = inComponent(vsi, i, system, code, valueset.getCompose().getInclude().size() == 1, warnings); 619 i++; 620 if (ok == null && result == false) { 621 result = null; 622 } else if (ok) { 623 result = true; 624 break; 625 } 626 } 627 for (ConceptSetComponent vsi : valueset.getCompose().getExclude()) { 628 Boolean nok = inComponent(vsi, i, system, code, valueset.getCompose().getInclude().size() == 1, warnings); 629 i++; 630 if (nok == null && result == false) { 631 result = null; 632 } else if (nok != null && nok) { 633 result = false; 634 } 635 } 636 } 637 638 return result; 639 } 640 641 private Boolean inComponent(ConceptSetComponent vsi, int vsiIndex, String system, String code, boolean only, List<String> warnings) throws FHIRException { 642 for (UriType uri : vsi.getValueSet()) { 643 if (inImport(uri.getValue(), system, code)) { 644 return true; 645 } 646 } 647 648 if (!vsi.hasSystem()) { 649 return false; 650 } 651 if (only && system == null) { 652 // whether we know the system or not, we'll accept the stated codes at face value 653 for (ConceptReferenceComponent cc : vsi.getConcept()) { 654 if (cc.getCode().equals(code)) { 655 return true; 656 } 657 } 658 } 659 660 if (!system.equals(vsi.getSystem())) 661 return false; 662 // ok, we need the code system 663 CodeSystem cs = resolveCodeSystem(system); 664 if (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)) { 665 // make up a transient value set with 666 ValueSet vs = new ValueSet(); 667 vs.setStatus(PublicationStatus.ACTIVE); 668 vs.setUrl(valueset.getUrl()+"--"+vsiIndex); 669 vs.setVersion(valueset.getVersion()); 670 vs.getCompose().addInclude(vsi); 671 ValidationResult res = context.validateCode(options.noClient(), new Coding(system, code, null), vs); 672 if (res.getErrorClass() == TerminologyServiceErrorClass.UNKNOWN || res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED || res.getErrorClass() == TerminologyServiceErrorClass.VALUESET_UNSUPPORTED) { 673 if (warnings != null && res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) { 674 warnings.add(context.formatMessage(I18nConstants.TERMINOLOGY_TX_SYSTEM_NOTKNOWN, system)); 675 } 676 return null; 677 } 678 if (res.getErrorClass() == TerminologyServiceErrorClass.NOSERVICE) { 679 throw new NoTerminologyServiceException(); 680 } 681 return res.isOk(); 682 } else { 683 if (vsi.hasFilter()) { 684 boolean ok = true; 685 for (ConceptSetFilterComponent f : vsi.getFilter()) { 686 if (!codeInFilter(cs, system, f, code)) { 687 ok = false; 688 break; 689 } 690 } 691 return ok; 692 } 693 694 List<ConceptDefinitionComponent> list = cs.getConcept(); 695 boolean ok = validateCodeInConceptList(code, cs, list); 696 if (ok && vsi.hasConcept()) { 697 for (ConceptReferenceComponent cc : vsi.getConcept()) { 698 if (cc.getCode().equals(code)) { 699 return true; 700 } 701 } 702 return false; 703 } else { 704 return ok; 705 } 706 } 707 } 708 709 private boolean codeInFilter(CodeSystem cs, String system, ConceptSetFilterComponent f, String code) throws FHIRException { 710 if ("concept".equals(f.getProperty())) 711 return codeInConceptFilter(cs, f, code); 712 else { 713 System.out.println("todo: handle filters with property = "+f.getProperty()); 714 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__FILTER_WITH_PROPERTY__, cs.getUrl(), f.getProperty())); 715 } 716 } 717 718 private boolean codeInConceptFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) throws FHIRException { 719 switch (f.getOp()) { 720 case ISA: return codeInConceptIsAFilter(cs, f, code, false); 721 case ISNOTA: return !codeInConceptIsAFilter(cs, f, code, false); 722 case DESCENDENTOF: return codeInConceptIsAFilter(cs, f, code, true); 723 default: 724 System.out.println("todo: handle concept filters with op = "+f.getOp()); 725 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__CONCEPT_FILTER_WITH_OP__, cs.getUrl(), f.getOp())); 726 } 727 } 728 729 private boolean codeInConceptIsAFilter(CodeSystem cs, ConceptSetFilterComponent f, String code, boolean rootOnly) { 730 if (!rootOnly && code.equals(f.getProperty())) { 731 return true; 732 } 733 ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), f.getValue()); 734 if (cc == null) { 735 return false; 736 } 737 cc = findCodeInConcept(cc, code); 738 return cc != null; 739 } 740 741 public boolean validateCodeInConceptList(String code, CodeSystem def, List<ConceptDefinitionComponent> list) { 742 if (def.getCaseSensitive()) { 743 for (ConceptDefinitionComponent cc : list) { 744 if (cc.getCode().equals(code)) { 745 return true; 746 } 747 if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept())) { 748 return true; 749 } 750 } 751 } else { 752 for (ConceptDefinitionComponent cc : list) { 753 if (cc.getCode().equalsIgnoreCase(code)) { 754 return true; 755 } 756 if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept())) { 757 return true; 758 } 759 } 760 } 761 return false; 762 } 763 764 private ValueSetCheckerSimple getVs(String url) { 765 if (inner.containsKey(url)) { 766 return inner.get(url); 767 } 768 ValueSet vs = context.fetchResource(ValueSet.class, url); 769 ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, context, localContext); 770 inner.put(url, vsc); 771 return vsc; 772 } 773 774 private boolean inImport(String uri, String system, String code) throws FHIRException { 775 ValueSetCheckerSimple vs = getVs(uri); 776 if (vs == null) { 777 return false; 778 } else { 779 Boolean ok = vs.codeInValueSet(system, code, null); 780 return ok != null && ok; 781 } 782 } 783 784}