001package org.hl7.fhir.r5.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.r5.context.IWorkerContext; 042import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult; 043import org.hl7.fhir.r5.model.CanonicalType; 044import org.hl7.fhir.r5.model.CodeSystem; 045import org.hl7.fhir.r5.model.CodeSystem.CodeSystemContentMode; 046import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; 047import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionDesignationComponent; 048import org.hl7.fhir.r5.model.CodeableConcept; 049import org.hl7.fhir.r5.model.Coding; 050import org.hl7.fhir.r5.model.Enumerations.PublicationStatus; 051import org.hl7.fhir.r5.model.UriType; 052import org.hl7.fhir.r5.model.ValueSet; 053import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent; 054import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceDesignationComponent; 055import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; 056import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent; 057import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; 058import org.hl7.fhir.r5.terminologies.ValueSetExpander.TerminologyServiceErrorClass; 059import org.hl7.fhir.r5.utils.ToolingExtensions; 060import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier; 061import org.hl7.fhir.r5.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 Coding foundCoding = null; 146 if (valueset != null && options.getValueSetMode() != ValueSetMode.NO_MEMBERSHIP_CHECK) { 147 Boolean result = false; 148 for (Coding c : code.getCoding()) { 149 Boolean ok = codeInValueSet(c.getSystem(), c.getCode(), warnings); 150 if (ok == null && result == false) { 151 result = null; 152 } else if (ok) { 153 result = true; 154 foundCoding = c; 155 } 156 } 157 if (result == null) { 158 warnings.add(0, context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl())); 159 } else if (!result) { 160 errors.add(0, context.formatMessage(I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl())); 161 } 162 } 163 if (errors.size() > 0) { 164 return new ValidationResult(IssueSeverity.ERROR, errors.toString()); 165 } else if (warnings.size() > 0) { 166 return new ValidationResult(IssueSeverity.WARNING, warnings.toString()); 167 } else { 168 ConceptDefinitionComponent cd = new ConceptDefinitionComponent(foundCoding.getCode()); 169 cd.setDisplay(foundCoding.getDisplay()); 170 return new ValidationResult(foundCoding.getSystem(), cd); 171 } 172 } 173 174 public CodeSystem resolveCodeSystem(String system) { 175 for (CodeSystem t : localSystems) { 176 if (t.getUrl().equals(system)) { 177 return t; 178 } 179 } 180 CodeSystem cs = context.fetchCodeSystem(system); 181 if (cs == null) { 182 cs = findSpecialCodeSystem(system); 183 } 184 return cs; 185 } 186 187 public ValidationResult validateCode(Coding code) throws FHIRException { 188 String warningMessage = null; 189 // first, we validate the concept itself 190 191 ValidationResult res = null; 192 boolean inExpansion = false; 193 boolean inInclude = false; 194 String system = code.hasSystem() ? code.getSystem() : getValueSetSystemOrNull(); 195 if (options.getValueSetMode() != ValueSetMode.CHECK_MEMERSHIP_ONLY) { 196 if (system == null && !code.hasDisplay()) { // dealing with just a plain code (enum) 197 system = systemForCodeInValueSet(code.getCode()); 198 } 199 if (!code.hasSystem()) { 200 if (options.isGuessSystem() && system == null && Utilities.isAbsoluteUrl(code.getCode())) { 201 system = "urn:ietf:rfc:3986"; // this arises when using URIs bound to value sets 202 } 203 code.setSystem(system); 204 } 205 inExpansion = checkExpansion(code); 206 inInclude = checkInclude(code); 207 CodeSystem cs = resolveCodeSystem(system); 208 if (cs == null) { 209 warningMessage = "Unable to resolve system "+system; 210 if (!inExpansion) { 211 if (valueset != null && valueset.hasExpansion()) { 212 return new ValidationResult(IssueSeverity.ERROR, context.formatMessage(I18nConstants.CODESYSTEM_CS_UNK_EXPANSION, valueset.getUrl(), code.getCode().toString(), code.getSystem())); 213 } else { 214 throw new FHIRException(warningMessage); 215 } 216 } 217 } 218 if (cs != null && cs.hasSupplements()) { 219 return new ValidationResult(IssueSeverity.ERROR, context.formatMessage(I18nConstants.CODESYSTEM_CS_NO_SUPPLEMENT, cs.getUrl())); 220 } 221 if (cs!=null && cs.getContent() != CodeSystemContentMode.COMPLETE) { 222 warningMessage = "Resolved system "+system+", but the definition is not complete"; 223 if (!inExpansion && cs.getContent() != CodeSystemContentMode.FRAGMENT) { // we're going to give it a go if it's a fragment 224 throw new FHIRException(warningMessage); 225 } 226 } 227 228 if (cs != null /*&& (cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT)*/) { 229 if (!(cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT)) { 230 // we can't validate that here. 231 throw new FHIRException("Unable to evaluate based on empty code system"); 232 } 233 res = validateCode(code, cs); 234 } else if (cs == null && valueset.hasExpansion() && inExpansion) { 235 // we just take the value set as face value then 236 res = new ValidationResult(system, new ConceptDefinitionComponent().setCode(code.getCode()).setDisplay(code.getDisplay())); 237 } else { 238 // well, we didn't find a code system - try the expansion? 239 // disabled waiting for discussion 240 throw new FHIRException("No try the server"); 241 } 242 } else { 243 inExpansion = checkExpansion(code); 244 inInclude = checkInclude(code); 245 } 246 247 List<String> warnings = new ArrayList<>(); 248 249 // then, if we have a value set, we check it's in the value set 250 if (valueset != null && options.getValueSetMode() != ValueSetMode.NO_MEMBERSHIP_CHECK) { 251 if ((res==null || res.isOk())) { 252 Boolean ok = codeInValueSet(system, code.getCode(), warnings); 253 if (ok == null || !ok) { 254 if (res == null) { 255 res = new ValidationResult((IssueSeverity) null, null); 256 } 257 if (!inExpansion && !inInclude) { 258 res.setMessage("Not in value set "+valueset.getUrl()).setSeverity(IssueSeverity.ERROR); 259 } else if (warningMessage!=null) { 260 res = new ValidationResult(IssueSeverity.WARNING, context.formatMessage(I18nConstants.CODE_FOUND_IN_EXPANSION_HOWEVER_, warningMessage)); 261 } else if (inExpansion) { 262 res.setMessage("Code found in expansion, however: " + res.getMessage()); 263 } else if (inInclude) { 264 res.setMessage("Code found in include, however: " + res.getMessage()); 265 } 266 } 267 } 268 } 269 return res; 270 } 271 272 private boolean checkInclude(Coding code) { 273 if (valueset == null || code.getSystem() == null || code.getCode() == null) { 274 return false; 275 } 276 for (ConceptSetComponent inc : valueset.getCompose().getExclude()) { 277 if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) { 278 for (ConceptReferenceComponent cc : inc.getConcept()) { 279 if (cc.hasCode() && cc.getCode().equals(code.getCode())) { 280 return false; 281 } 282 } 283 } 284 } 285 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 286 if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) { 287 for (ConceptReferenceComponent cc : inc.getConcept()) { 288 if (cc.hasCode() && cc.getCode().equals(code.getCode())) { 289 return true; 290 } 291 } 292 } 293 } 294 return false; 295 } 296 297 private CodeSystem findSpecialCodeSystem(String system) { 298 if ("urn:ietf:rfc:3986".equals(system)) { 299 CodeSystem cs = new CodeSystem(); 300 cs.setUrl(system); 301 cs.setUserData("tx.cs.special", new URICodeSystem()); 302 cs.setContent(CodeSystemContentMode.COMPLETE); 303 return cs; 304 } 305 return null; 306 } 307 308 private ValidationResult findCodeInExpansion(Coding code) { 309 if (valueset==null || !valueset.hasExpansion()) 310 return null; 311 return findCodeInExpansion(code, valueset.getExpansion().getContains()); 312 } 313 314 private ValidationResult findCodeInExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) { 315 for (ValueSetExpansionContainsComponent containsComponent: contains) { 316 if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) { 317 ConceptDefinitionComponent ccd = new ConceptDefinitionComponent(); 318 ccd.setCode(containsComponent.getCode()); 319 ccd.setDisplay(containsComponent.getDisplay()); 320 ValidationResult res = new ValidationResult(code.getSystem(), ccd); 321 return res; 322 } 323 if (containsComponent.hasContains()) { 324 ValidationResult res = findCodeInExpansion(code, containsComponent.getContains()); 325 if (res != null) { 326 return res; 327 } 328 } 329 } 330 return null; 331 } 332 333 private boolean checkExpansion(Coding code) { 334 if (valueset==null || !valueset.hasExpansion()) { 335 return false; 336 } 337 return checkExpansion(code, valueset.getExpansion().getContains()); 338 } 339 340 private boolean checkExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) { 341 for (ValueSetExpansionContainsComponent containsComponent: contains) { 342 if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) { 343 return true; 344 } 345 if (containsComponent.hasContains() && checkExpansion(code, containsComponent.getContains())) { 346 return true; 347 } 348 } 349 return false; 350 } 351 352 private ValidationResult validateCode(Coding code, CodeSystem cs) { 353 ConceptDefinitionComponent cc = cs.hasUserData("tx.cs.special") ? ((SpecialCodeSystem) cs.getUserData("tx.cs.special")).findConcept(code) : findCodeInConcept(cs.getConcept(), code.getCode()); 354 if (cc == null) { 355 if (cs.getContent() == CodeSystemContentMode.FRAGMENT) { 356 return new ValidationResult(IssueSeverity.WARNING, context.formatMessage(I18nConstants.UNKNOWN_CODE__IN_FRAGMENT, gen(code), cs.getUrl())); 357 } else { 358 return new ValidationResult(IssueSeverity.ERROR, context.formatMessage(I18nConstants.UNKNOWN_CODE__IN_, gen(code), cs.getUrl())); 359 } 360 } 361 if (code.getDisplay() == null) { 362 return new ValidationResult(code.getSystem(), cc); 363 } 364 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 365 if (cc.hasDisplay()) { 366 b.append(cc.getDisplay()); 367 if (code.getDisplay().equalsIgnoreCase(cc.getDisplay())) { 368 return new ValidationResult(code.getSystem(), cc); 369 } 370 } 371 for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) { 372 b.append(ds.getValue()); 373 if (code.getDisplay().equalsIgnoreCase(ds.getValue())) { 374 return new ValidationResult(code.getSystem(), cc); 375 } 376 } 377 // also check to see if the value set has another display 378 ConceptReferenceComponent vs = findValueSetRef(code.getSystem(), code.getCode()); 379 if (vs != null && (vs.hasDisplay() ||vs.hasDesignation())) { 380 if (vs.hasDisplay()) { 381 b.append(vs.getDisplay()); 382 if (code.getDisplay().equalsIgnoreCase(vs.getDisplay())) { 383 return new ValidationResult(code.getSystem(), cc); 384 } 385 } 386 for (ConceptReferenceDesignationComponent ds : vs.getDesignation()) { 387 b.append(ds.getValue()); 388 if (code.getDisplay().equalsIgnoreCase(ds.getValue())) { 389 return new ValidationResult(code.getSystem(), cc); 390 } 391 } 392 } 393 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); 394 } 395 396 private ConceptReferenceComponent findValueSetRef(String system, String code) { 397 if (valueset == null) 398 return null; 399 // if it has an expansion 400 for (ValueSetExpansionContainsComponent exp : valueset.getExpansion().getContains()) { 401 if (system.equals(exp.getSystem()) && code.equals(exp.getCode())) { 402 ConceptReferenceComponent cc = new ConceptReferenceComponent(); 403 cc.setDisplay(exp.getDisplay()); 404 cc.setDesignation(exp.getDesignation()); 405 return cc; 406 } 407 } 408 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 409 if (system.equals(inc.getSystem())) { 410 for (ConceptReferenceComponent cc : inc.getConcept()) { 411 if (cc.getCode().equals(code)) { 412 return cc; 413 } 414 } 415 } 416 for (CanonicalType url : inc.getValueSet()) { 417 ConceptReferenceComponent cc = getVs(url.asStringValue()).findValueSetRef(system, code); 418 if (cc != null) { 419 return cc; 420 } 421 } 422 } 423 return null; 424 } 425 426 private String gen(Coding code) { 427 if (code.hasSystem()) { 428 return code.getSystem()+"#"+code.getCode(); 429 } else { 430 return null; 431 } 432 } 433 434 private String getValueSetSystem() throws FHIRException { 435 if (valueset == null) { 436 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__NO_VALUE_SET)); 437 } 438 if (valueset.getCompose().getInclude().size() == 0) { 439 if (!valueset.hasExpansion() || valueset.getExpansion().getContains().size() == 0) { 440 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_NO_INCLUDES_OR_EXPANSION)); 441 } else { 442 String cs = valueset.getExpansion().getContains().get(0).getSystem(); 443 if (cs != null && checkSystem(valueset.getExpansion().getContains(), cs)) { 444 return cs; 445 } else { 446 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_EXPANSION_HAS_MULTIPLE_SYSTEMS)); 447 } 448 } 449 } 450 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 451 if (inc.hasValueSet()) { 452 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_IMPORTS)); 453 } 454 if (!inc.hasSystem()) { 455 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_NO_SYSTEM)); 456 } 457 } 458 if (valueset.getCompose().getInclude().size() == 1) { 459 return valueset.getCompose().getInclude().get(0).getSystem(); 460 } 461 462 return null; 463 } 464 465 private String getValueSetSystemOrNull() throws FHIRException { 466 if (valueset == null) { 467 return null; 468 } 469 if (valueset.getCompose().getInclude().size() == 0) { 470 if (!valueset.hasExpansion() || valueset.getExpansion().getContains().size() == 0) { 471 return null; 472 } else { 473 String cs = valueset.getExpansion().getContains().get(0).getSystem(); 474 if (cs != null && checkSystem(valueset.getExpansion().getContains(), cs)) { 475 return cs; 476 } else { 477 return null; 478 } 479 } 480 } 481 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 482 if (inc.hasValueSet()) { 483 return null; 484 } 485 if (!inc.hasSystem()) { 486 return null; 487 } 488 } 489 if (valueset.getCompose().getInclude().size() == 1) { 490 return valueset.getCompose().getInclude().get(0).getSystem(); 491 } 492 493 return null; 494 } 495 496 /* 497 * Check that all system values within an expansion correspond to the specified system value 498 */ 499 private boolean checkSystem(List<ValueSetExpansionContainsComponent> containsList, String system) { 500 for (ValueSetExpansionContainsComponent contains : containsList) { 501 if (!contains.getSystem().equals(system) || (contains.hasContains() && !checkSystem(contains.getContains(), system))) { 502 return false; 503 } 504 } 505 return true; 506 } 507 508 private ConceptDefinitionComponent findCodeInConcept(ConceptDefinitionComponent concept, String code) { 509 if (code.equals(concept.getCode())) { 510 return concept; 511 } 512 ConceptDefinitionComponent cc = findCodeInConcept(concept.getConcept(), code); 513 if (cc != null) { 514 return cc; 515 } 516 if (concept.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) { 517 List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) concept.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK); 518 for (ConceptDefinitionComponent c : children) { 519 cc = findCodeInConcept(c, code); 520 if (cc != null) { 521 return cc; 522 } 523 } 524 } 525 return null; 526 } 527 528 private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code) { 529 for (ConceptDefinitionComponent cc : concept) { 530 if (code.equals(cc.getCode())) { 531 return cc; 532 } 533 ConceptDefinitionComponent c = findCodeInConcept(cc, code); 534 if (c != null) { 535 return c; 536 } 537 } 538 return null; 539 } 540 541 542 private String systemForCodeInValueSet(String code) { 543 String sys = null; 544 if (valueset.hasCompose()) { 545 if (valueset.getCompose().hasExclude()) { 546 return null; 547 } 548 for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) { 549 if (vsi.hasValueSet()) { 550 return null; 551 } 552 if (!vsi.hasSystem()) { 553 return null; 554 } 555 if (vsi.hasFilter()) { 556 return null; 557 } 558 CodeSystem cs = resolveCodeSystem(vsi.getSystem()); 559 if (cs == null) { 560 return null; 561 } 562 if (vsi.hasConcept()) { 563 for (ConceptReferenceComponent cc : vsi.getConcept()) { 564 boolean match = cs.getCaseSensitive() ? cc.getCode().equals(code) : cc.getCode().equalsIgnoreCase(code); 565 if (match) { 566 if (sys == null) { 567 sys = vsi.getSystem(); 568 } else if (!sys.equals(vsi.getSystem())) { 569 return null; 570 } 571 } 572 } 573 } else { 574 ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code); 575 if (cc != null) { 576 if (sys == null) { 577 sys = vsi.getSystem(); 578 } else if (!sys.equals(vsi.getSystem())) { 579 return null; 580 } 581 } 582 } 583 } 584 } else if (valueset.hasExpansion()) { 585 // Retrieve a list of all systems associated with this code in the expansion 586 List<String> systems = new ArrayList<String>(); 587 checkSystems(valueset.getExpansion().getContains(), code, systems); 588 if (systems.size()==1) 589 sys = systems.get(0); 590 } 591 592 return sys; 593 } 594 595 /* 596 * Recursively go through all codes in the expansion and for any coding that matches the specified code, add the system for that coding 597 * to the passed list. 598 */ 599 private void checkSystems(List<ValueSetExpansionContainsComponent> contains, String code, List<String> systems) { 600 for (ValueSetExpansionContainsComponent c: contains) { 601 if (c.getCode().equals(code)) { 602 if (!systems.contains(c.getSystem())) 603 systems.add(c.getSystem()); 604 } 605 if (c.hasContains()) 606 checkSystems(c.getContains(), code, systems); 607 } 608 } 609 610 @Override 611 public Boolean codeInValueSet(String system, String code, List<String> warnings) throws FHIRException { 612 if (valueset == null) { 613 return false; 614 } 615 Boolean result = false; 616 617 if (valueset.hasExpansion()) { 618 return checkExpansion(new Coding(system, code, null)); 619 } else if (valueset.hasCompose()) { 620 int i = 0; 621 for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) { 622 Boolean ok = inComponent(vsi, i, system, code, valueset.getCompose().getInclude().size() == 1, warnings); 623 i++; 624 if (ok == null && result == false) { 625 result = null; 626 } else if (ok) { 627 result = true; 628 break; 629 } 630 } 631 i = valueset.getCompose().getInclude().size(); 632 for (ConceptSetComponent vsi : valueset.getCompose().getExclude()) { 633 Boolean nok = inComponent(vsi, i, system, code, valueset.getCompose().getInclude().size() == 1, warnings); 634 i++; 635 if (nok == null && result == false) { 636 result = null; 637 } else if (nok != null && nok) { 638 result = false; 639 } 640 } 641 } 642 643 return result; 644 } 645 646 private Boolean inComponent(ConceptSetComponent vsi, int vsiIndex, String system, String code, boolean only, List<String> warnings) throws FHIRException { 647 for (UriType uri : vsi.getValueSet()) { 648 if (inImport(uri.getValue(), system, code)) { 649 return true; 650 } 651 } 652 653 if (!vsi.hasSystem()) { 654 return false; 655 } 656 if (only && system == null) { 657 // whether we know the system or not, we'll accept the stated codes at face value 658 for (ConceptReferenceComponent cc : vsi.getConcept()) { 659 if (cc.getCode().equals(code)) { 660 return true; 661 } 662 } 663 } 664 665 if (!system.equals(vsi.getSystem())) 666 return false; 667 // ok, we need the code system 668 CodeSystem cs = resolveCodeSystem(system); 669 if (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)) { 670 // make up a transient value set with 671 ValueSet vs = new ValueSet(); 672 vs.setStatus(PublicationStatus.ACTIVE); 673 vs.setUrl(valueset.getUrl()+"--"+vsiIndex); 674 vs.setVersion(valueset.getVersion()); 675 vs.getCompose().addInclude(vsi); 676 ValidationResult res = context.validateCode(options.noClient(), new Coding(system, code, null), vs); 677 if (res.getErrorClass() == TerminologyServiceErrorClass.UNKNOWN || res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED || res.getErrorClass() == TerminologyServiceErrorClass.VALUESET_UNSUPPORTED) { 678 if (warnings != null && res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) { 679 warnings.add(context.formatMessage(I18nConstants.TERMINOLOGY_TX_SYSTEM_NOTKNOWN, system)); 680 } 681 return null; 682 } 683 if (res.getErrorClass() == TerminologyServiceErrorClass.NOSERVICE) { 684 throw new NoTerminologyServiceException(); 685 } 686 return res.isOk(); 687 } else { 688 if (vsi.hasFilter()) { 689 boolean ok = true; 690 for (ConceptSetFilterComponent f : vsi.getFilter()) { 691 if (!codeInFilter(cs, system, f, code)) { 692 ok = false; 693 break; 694 } 695 } 696 return ok; 697 } 698 699 List<ConceptDefinitionComponent> list = cs.getConcept(); 700 boolean ok = validateCodeInConceptList(code, cs, list); 701 if (ok && vsi.hasConcept()) { 702 for (ConceptReferenceComponent cc : vsi.getConcept()) { 703 if (cc.getCode().equals(code)) { 704 return true; 705 } 706 } 707 return false; 708 } else { 709 return ok; 710 } 711 } 712 } 713 714 private boolean codeInFilter(CodeSystem cs, String system, ConceptSetFilterComponent f, String code) throws FHIRException { 715 if ("concept".equals(f.getProperty())) 716 return codeInConceptFilter(cs, f, code); 717 else { 718 System.out.println("todo: handle filters with property = "+f.getProperty()); 719 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__FILTER_WITH_PROPERTY__, cs.getUrl(), f.getProperty())); 720 } 721 } 722 723 private boolean codeInConceptFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) throws FHIRException { 724 switch (f.getOp()) { 725 case ISA: return codeInConceptIsAFilter(cs, f, code, false); 726 case ISNOTA: return !codeInConceptIsAFilter(cs, f, code, false); 727 case DESCENDENTOF: return codeInConceptIsAFilter(cs, f, code, true); 728 default: 729 System.out.println("todo: handle concept filters with op = "+f.getOp()); 730 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__CONCEPT_FILTER_WITH_OP__, cs.getUrl(), f.getOp())); 731 } 732 } 733 734 private boolean codeInConceptIsAFilter(CodeSystem cs, ConceptSetFilterComponent f, String code, boolean rootOnly) { 735 if (!rootOnly && code.equals(f.getProperty())) { 736 return true; 737 } 738 ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), f.getValue()); 739 if (cc == null) { 740 return false; 741 } 742 cc = findCodeInConcept(cc, code); 743 return cc != null; 744 } 745 746 public boolean validateCodeInConceptList(String code, CodeSystem def, List<ConceptDefinitionComponent> list) { 747 if (def.getCaseSensitive()) { 748 for (ConceptDefinitionComponent cc : list) { 749 if (cc.getCode().equals(code)) { 750 return true; 751 } 752 if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept())) { 753 return true; 754 } 755 } 756 } else { 757 for (ConceptDefinitionComponent cc : list) { 758 if (cc.getCode().equalsIgnoreCase(code)) { 759 return true; 760 } 761 if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept())) { 762 return true; 763 } 764 } 765 } 766 return false; 767 } 768 769 private ValueSetCheckerSimple getVs(String url) { 770 if (inner.containsKey(url)) { 771 return inner.get(url); 772 } 773 ValueSet vs = context.fetchResource(ValueSet.class, url); 774 ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, context, localContext); 775 inner.put(url, vsc); 776 return vsc; 777 } 778 779 private boolean inImport(String uri, String system, String code) throws FHIRException { 780 ValueSetCheckerSimple vs = getVs(uri); 781 if (vs == null) { 782 return false; 783 } else { 784 Boolean ok = vs.codeInValueSet(system, code, null); 785 return ok != null && ok; 786 } 787 } 788 789}