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.Calendar; 036import java.util.GregorianCalendar; 037import java.util.HashMap; 038import java.util.HashSet; 039import java.util.List; 040import java.util.Map; 041import java.util.Set; 042 043import org.hl7.fhir.exceptions.FHIRException; 044import org.hl7.fhir.exceptions.NoTerminologyServiceException; 045import org.hl7.fhir.r5.context.IWorkerContext; 046import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult; 047import org.hl7.fhir.r5.model.CanonicalType; 048import org.hl7.fhir.r5.model.CodeSystem; 049import org.hl7.fhir.r5.model.CodeSystem.CodeSystemContentMode; 050import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; 051import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionDesignationComponent; 052import org.hl7.fhir.r5.model.CodeableConcept; 053import org.hl7.fhir.r5.model.Coding; 054import org.hl7.fhir.r5.model.Enumerations.PublicationStatus; 055import org.hl7.fhir.r5.model.PackageInformation; 056import org.hl7.fhir.r5.model.UriType; 057import org.hl7.fhir.r5.model.ValueSet; 058import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent; 059import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceDesignationComponent; 060import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; 061import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent; 062import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; 063import org.hl7.fhir.r5.terminologies.ValueSetExpander.TerminologyServiceErrorClass; 064import org.hl7.fhir.r5.terminologies.ValueSetExpander.ValueSetExpansionOutcome; 065import org.hl7.fhir.r5.utils.ToolingExtensions; 066import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier; 067import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier.ValidationContextResourceProxy; 068import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 069import org.hl7.fhir.utilities.Utilities; 070import org.hl7.fhir.utilities.VersionUtilities; 071import org.hl7.fhir.utilities.i18n.I18nConstants; 072import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 073import org.hl7.fhir.utilities.validation.ValidationOptions; 074import org.hl7.fhir.utilities.validation.ValidationOptions.ValueSetMode; 075 076public class ValueSetCheckerSimple extends ValueSetWorker implements ValueSetChecker { 077 078 private ValueSet valueset; 079 private IWorkerContext context; 080 private Map<String, ValueSetCheckerSimple> inner = new HashMap<>(); 081 private ValidationOptions options; 082 private ValidationContextCarrier localContext; 083 private List<CodeSystem> localSystems = new ArrayList<>(); 084 085 public ValueSetCheckerSimple(ValidationOptions options, ValueSet source, IWorkerContext context) { 086 this.valueset = source; 087 this.context = context; 088 this.options = options; 089 } 090 091 public ValueSetCheckerSimple(ValidationOptions options, ValueSet source, IWorkerContext context, ValidationContextCarrier ctxt) { 092 this.valueset = source; 093 this.context = context; 094 this.options = options; 095 this.localContext = ctxt; 096 analyseValueSet(); 097 } 098 099 private void analyseValueSet() { 100 if (localContext != null) { 101 if (valueset != null) { 102 for (ConceptSetComponent i : valueset.getCompose().getInclude()) { 103 analyseComponent(i); 104 } 105 for (ConceptSetComponent i : valueset.getCompose().getExclude()) { 106 analyseComponent(i); 107 } 108 } 109 } 110 } 111 112 private void analyseComponent(ConceptSetComponent i) { 113 if (i.getSystemElement().hasExtension(ToolingExtensions.EXT_VALUESET_SYSTEM)) { 114 String ref = i.getSystemElement().getExtensionString(ToolingExtensions.EXT_VALUESET_SYSTEM); 115 if (ref.startsWith("#")) { 116 String id = ref.substring(1); 117 for (ValidationContextResourceProxy t : localContext.getResources()) { 118 CodeSystem cs = (CodeSystem) t.loadContainedResource(id, CodeSystem.class); 119 if (cs != null) { 120 localSystems.add(cs); 121 } 122 } 123 } else { 124 throw new Error("Not done yet #2: "+ref); 125 } 126 } 127 } 128 129 public ValidationResult validateCode(CodeableConcept code) throws FHIRException { 130 // first, we validate the codings themselves 131 List<String> errors = new ArrayList<String>(); 132 ValidationProcessInfo info = new ValidationProcessInfo(); 133 if (options.getValueSetMode() != ValueSetMode.CHECK_MEMERSHIP_ONLY) { 134 for (Coding c : code.getCoding()) { 135 if (!c.hasSystem()) { 136 info.getWarnings().add(context.formatMessage(I18nConstants.CODING_HAS_NO_SYSTEM__CANNOT_VALIDATE)); 137 } 138 CodeSystem cs = resolveCodeSystem(c.getSystem(), c.getVersion()); 139 ValidationResult res = null; 140 if (cs == null || cs.getContent() != CodeSystemContentMode.COMPLETE) { 141 res = context.validateCode(options.noClient(), c, null); 142 } else { 143 res = validateCode(c, cs); 144 } 145 if (!res.isOk()) { 146 errors.add(res.getMessage()); 147 } else if (res.getMessage() != null) { 148 info.getWarnings().add(res.getMessage()); 149 } 150 } 151 } 152 Coding foundCoding = null; 153 if (valueset != null && options.getValueSetMode() != ValueSetMode.NO_MEMBERSHIP_CHECK) { 154 Boolean result = false; 155 for (Coding c : code.getCoding()) { 156 Boolean ok = codeInValueSet(c.getSystem(), c.getVersion(), c.getCode(), info); 157 if (ok == null && result == false) { 158 result = null; 159 } else if (ok) { 160 result = true; 161 foundCoding = c; 162 } 163 } 164 if (result == null) { 165 info.getWarnings().add(0, context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl())); 166 } else if (!result) { 167 errors.add(0, context.formatMessage(I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl())); 168 } 169 } 170 if (errors.size() > 0) { 171 return new ValidationResult(IssueSeverity.ERROR, errors.toString()); 172 } else if (info.getWarnings().size() > 0) { 173 return new ValidationResult(IssueSeverity.WARNING, info.getWarnings().toString()); 174 } else { 175 ConceptDefinitionComponent cd = new ConceptDefinitionComponent(foundCoding.getCode()); 176 cd.setDisplay(foundCoding.getDisplay()); 177 return new ValidationResult(foundCoding.getSystem(), cd); 178 } 179 } 180 181 public CodeSystem resolveCodeSystem(String system, String version) { 182 for (CodeSystem t : localSystems) { 183 if (t.getUrl().equals(system) && versionsMatch(version, t.getVersion())) { 184 return t; 185 } 186 } 187 CodeSystem cs = context.fetchCodeSystem(system, version); 188 if (cs == null) { 189 cs = findSpecialCodeSystem(system, version); 190 } 191 return cs; 192 } 193 194 private boolean versionsMatch(String versionTest, String versionActual) { 195 return versionTest == null && VersionUtilities.versionsMatch(versionTest, versionActual); 196 } 197 198 public ValidationResult validateCode(Coding code) throws FHIRException { 199 String warningMessage = null; 200 // first, we validate the concept itself 201 202 ValidationResult res = null; 203 boolean inExpansion = false; 204 boolean inInclude = false; 205 String system = code.hasSystem() ? code.getSystem() : getValueSetSystemOrNull(); 206 if (options.getValueSetMode() != ValueSetMode.CHECK_MEMERSHIP_ONLY) { 207 if (system == null && !code.hasDisplay()) { // dealing with just a plain code (enum) 208 List<String> problems = new ArrayList<>(); 209 system = systemForCodeInValueSet(code.getCode(), problems); 210 if (system == null) { 211 if (problems.size() == 0) { 212 throw new Error("Unable to resolve systems but no reason why"); // this is an error in the java code 213 } else if (problems.size() == 1) { 214 return new ValidationResult(IssueSeverity.ERROR, problems.get(0)); 215 } else { 216 return new ValidationResult(IssueSeverity.ERROR, problems.toString()); 217 } 218 } 219 } 220 if (!code.hasSystem()) { 221 if (options.isGuessSystem() && system == null && Utilities.isAbsoluteUrl(code.getCode())) { 222 system = "urn:ietf:rfc:3986"; // this arises when using URIs bound to value sets 223 } 224 code.setSystem(system); 225 } 226 inExpansion = checkExpansion(code); 227 inInclude = checkInclude(code); 228 CodeSystem cs = resolveCodeSystem(system, code.getVersion()); 229 if (cs == null) { 230 warningMessage = "Unable to resolve system "+system; 231 if (!inExpansion) { 232 if (valueset != null && valueset.hasExpansion()) { 233 return new ValidationResult(IssueSeverity.ERROR, context.formatMessage(I18nConstants.CODESYSTEM_CS_UNK_EXPANSION, valueset.getUrl(), code.getCode().toString(), code.getSystem())); 234 } else { 235 throw new FHIRException(warningMessage); 236 } 237 } 238 } 239 if (cs != null && cs.hasSupplements()) { 240 return new ValidationResult(IssueSeverity.ERROR, context.formatMessage(I18nConstants.CODESYSTEM_CS_NO_SUPPLEMENT, cs.getUrl())); 241 } 242 if (cs!=null && cs.getContent() != CodeSystemContentMode.COMPLETE) { 243 warningMessage = "Resolved system "+system+(cs.hasVersion() ? " (v"+cs.getVersion()+")" : "")+", but the definition is not complete"; 244 if (!inExpansion && cs.getContent() != CodeSystemContentMode.FRAGMENT) { // we're going to give it a go if it's a fragment 245 throw new FHIRException(warningMessage); 246 } 247 } 248 249 if (cs != null /*&& (cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT)*/) { 250 if (!(cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT)) { 251 if (inInclude) { 252 ConceptReferenceComponent cc = findInInclude(code); 253 if (cc != null) { 254 // we'll take it on faith 255 res = new ValidationResult(system, new ConceptDefinitionComponent().setCode(cc.getCode()).setDisplay(cc.getDisplay())); 256 res.setMessage("Resolved system "+system+", but the definition is not complete, so assuming value set include is correct"); 257 return res; 258 } 259 } 260 // we can't validate that here. 261 throw new FHIRException("Unable to evaluate based on empty code system"); 262 } 263 res = validateCode(code, cs); 264 } else if (cs == null && valueset.hasExpansion() && inExpansion) { 265 // we just take the value set as face value then 266 res = new ValidationResult(system, new ConceptDefinitionComponent().setCode(code.getCode()).setDisplay(code.getDisplay())); 267 } else { 268 // well, we didn't find a code system - try the expansion? 269 // disabled waiting for discussion 270 throw new FHIRException("No try the server"); 271 } 272 } else { 273 inExpansion = checkExpansion(code); 274 inInclude = checkInclude(code); 275 } 276 277 ValidationProcessInfo info = new ValidationProcessInfo(); 278 279 // then, if we have a value set, we check it's in the value set 280 if (valueset != null && options.getValueSetMode() != ValueSetMode.NO_MEMBERSHIP_CHECK) { 281 if ((res==null || res.isOk())) { 282 Boolean ok = codeInValueSet(system, code.getVersion(), code.getCode(), info); 283 if (ok == null || !ok) { 284 if (res == null) { 285 res = new ValidationResult((IssueSeverity) null, null); 286 } 287 if (info.getErr() != null) { 288 res.setErrorClass(info.getErr()); 289 } 290 if (ok == null) { 291 res.setMessage("Unable to check whether code is in value set "+valueset.getUrl()+": "+info.getWarnings()).setSeverity(IssueSeverity.WARNING); 292 } else if (!inExpansion && !inInclude) { 293 if (!info.getWarnings().isEmpty()) { 294 res.setMessage("Not in value set "+valueset.getUrl()+": "+info.getWarnings()).setSeverity(IssueSeverity.ERROR); 295 } else { 296 res.setMessage("Not in value set "+valueset.getUrl()).setSeverity(IssueSeverity.ERROR); 297 } 298 } else if (warningMessage!=null) { 299 res = new ValidationResult(IssueSeverity.WARNING, context.formatMessage(I18nConstants.CODE_FOUND_IN_EXPANSION_HOWEVER_, warningMessage)); 300 } else if (inExpansion) { 301 res.setMessage("Code found in expansion, however: " + res.getMessage()); 302 } else if (inInclude) { 303 res.setMessage("Code found in include, however: " + res.getMessage()); 304 } 305 } 306 } 307 } 308 return res; 309 } 310 311 private boolean checkInclude(Coding code) { 312 if (valueset == null || code.getSystem() == null || code.getCode() == null) { 313 return false; 314 } 315 for (ConceptSetComponent inc : valueset.getCompose().getExclude()) { 316 if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) { 317 for (ConceptReferenceComponent cc : inc.getConcept()) { 318 if (cc.hasCode() && cc.getCode().equals(code.getCode())) { 319 return false; 320 } 321 } 322 } 323 } 324 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 325 if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) { 326 for (ConceptReferenceComponent cc : inc.getConcept()) { 327 if (cc.hasCode() && cc.getCode().equals(code.getCode())) { 328 return true; 329 } 330 } 331 } 332 } 333 return false; 334 } 335 336 private ConceptReferenceComponent findInInclude(Coding code) { 337 if (valueset == null || code.getSystem() == null || code.getCode() == null) { 338 return null; 339 } 340 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 341 if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) { 342 for (ConceptReferenceComponent cc : inc.getConcept()) { 343 if (cc.hasCode() && cc.getCode().equals(code.getCode())) { 344 return cc; 345 } 346 } 347 } 348 } 349 return null; 350 } 351 352 private CodeSystem findSpecialCodeSystem(String system, String version) { 353 if ("urn:ietf:rfc:3986".equals(system)) { 354 CodeSystem cs = new CodeSystem(); 355 cs.setUrl(system); 356 cs.setUserData("tx.cs.special", new URICodeSystem()); 357 cs.setContent(CodeSystemContentMode.COMPLETE); 358 return cs; 359 } 360 return null; 361 } 362 363 private ValidationResult findCodeInExpansion(Coding code) { 364 if (valueset==null || !valueset.hasExpansion()) 365 return null; 366 return findCodeInExpansion(code, valueset.getExpansion().getContains()); 367 } 368 369 private ValidationResult findCodeInExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) { 370 for (ValueSetExpansionContainsComponent containsComponent: contains) { 371 if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) { 372 ConceptDefinitionComponent ccd = new ConceptDefinitionComponent(); 373 ccd.setCode(containsComponent.getCode()); 374 ccd.setDisplay(containsComponent.getDisplay()); 375 ValidationResult res = new ValidationResult(code.getSystem(), ccd); 376 return res; 377 } 378 if (containsComponent.hasContains()) { 379 ValidationResult res = findCodeInExpansion(code, containsComponent.getContains()); 380 if (res != null) { 381 return res; 382 } 383 } 384 } 385 return null; 386 } 387 388 private boolean checkExpansion(Coding code) { 389 if (valueset==null || !valueset.hasExpansion()) { 390 return false; 391 } 392 return checkExpansion(code, valueset.getExpansion().getContains()); 393 } 394 395 private boolean checkExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) { 396 for (ValueSetExpansionContainsComponent containsComponent: contains) { 397 if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) { 398 return true; 399 } 400 if (containsComponent.hasContains() && checkExpansion(code, containsComponent.getContains())) { 401 return true; 402 } 403 } 404 return false; 405 } 406 407 private ValidationResult validateCode(Coding code, CodeSystem cs) { 408 ConceptDefinitionComponent cc = cs.hasUserData("tx.cs.special") ? ((SpecialCodeSystem) cs.getUserData("tx.cs.special")).findConcept(code) : findCodeInConcept(cs.getConcept(), code.getCode()); 409 if (cc == null) { 410 if (cs.getContent() == CodeSystemContentMode.FRAGMENT) { 411 return new ValidationResult(IssueSeverity.WARNING, context.formatMessage(I18nConstants.UNKNOWN_CODE__IN_FRAGMENT, gen(code), cs.getUrl())); 412 } else { 413 return new ValidationResult(IssueSeverity.ERROR, context.formatMessage(I18nConstants.UNKNOWN_CODE__IN_, gen(code), cs.getUrl())); 414 } 415 } 416 if (code.getDisplay() == null) { 417 return new ValidationResult(code.getSystem(), cc); 418 } 419 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 420 if (cc.hasDisplay()) { 421 b.append(cc.getDisplay()); 422 if (code.getDisplay().equalsIgnoreCase(cc.getDisplay())) { 423 return new ValidationResult(code.getSystem(), cc); 424 } 425 } 426 for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) { 427 b.append(ds.getValue()); 428 if (code.getDisplay().equalsIgnoreCase(ds.getValue())) { 429 return new ValidationResult(code.getSystem(), cc); 430 } 431 } 432 // also check to see if the value set has another display 433 ConceptReferenceComponent vs = findValueSetRef(code.getSystem(), code.getCode()); 434 if (vs != null && (vs.hasDisplay() ||vs.hasDesignation())) { 435 if (vs.hasDisplay()) { 436 b.append(vs.getDisplay()); 437 if (code.getDisplay().equalsIgnoreCase(vs.getDisplay())) { 438 return new ValidationResult(code.getSystem(), cc); 439 } 440 } 441 for (ConceptReferenceDesignationComponent ds : vs.getDesignation()) { 442 b.append(ds.getValue()); 443 if (code.getDisplay().equalsIgnoreCase(ds.getValue())) { 444 return new ValidationResult(code.getSystem(), cc); 445 } 446 } 447 } 448 return new ValidationResult(IssueSeverity.WARNING, context.formatMessagePlural(b.count(), I18nConstants.DISPLAY_NAME_FOR__SHOULD_BE_ONE_OF__INSTEAD_OF, code.getSystem(), code.getCode(), b.toString(), code.getDisplay()), code.getSystem(), cc); 449 } 450 451 private ConceptReferenceComponent findValueSetRef(String system, String code) { 452 if (valueset == null) 453 return null; 454 // if it has an expansion 455 for (ValueSetExpansionContainsComponent exp : valueset.getExpansion().getContains()) { 456 if (system.equals(exp.getSystem()) && code.equals(exp.getCode())) { 457 ConceptReferenceComponent cc = new ConceptReferenceComponent(); 458 cc.setDisplay(exp.getDisplay()); 459 cc.setDesignation(exp.getDesignation()); 460 return cc; 461 } 462 } 463 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 464 if (system.equals(inc.getSystem())) { 465 for (ConceptReferenceComponent cc : inc.getConcept()) { 466 if (cc.getCode().equals(code)) { 467 return cc; 468 } 469 } 470 } 471 for (CanonicalType url : inc.getValueSet()) { 472 ConceptReferenceComponent cc = getVs(url.asStringValue()).findValueSetRef(system, code); 473 if (cc != null) { 474 return cc; 475 } 476 } 477 } 478 return null; 479 } 480 481 private String gen(Coding code) { 482 if (code.hasSystem()) { 483 return code.getSystem()+"#"+code.getCode(); 484 } else { 485 return null; 486 } 487 } 488 489 490 private String getValueSetSystemOrNull() throws FHIRException { 491 if (valueset == null) { 492 return null; 493 } 494 if (valueset.getCompose().getInclude().size() == 0) { 495 if (!valueset.hasExpansion() || valueset.getExpansion().getContains().size() == 0) { 496 return null; 497 } else { 498 String cs = valueset.getExpansion().getContains().get(0).getSystem(); 499 if (cs != null && checkSystem(valueset.getExpansion().getContains(), cs)) { 500 return cs; 501 } else { 502 return null; 503 } 504 } 505 } 506 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 507 if (inc.hasValueSet()) { 508 return null; 509 } 510 if (!inc.hasSystem()) { 511 return null; 512 } 513 } 514 if (valueset.getCompose().getInclude().size() == 1) { 515 return valueset.getCompose().getInclude().get(0).getSystem(); 516 } 517 518 return null; 519 } 520 521 /* 522 * Check that all system values within an expansion correspond to the specified system value 523 */ 524 private boolean checkSystem(List<ValueSetExpansionContainsComponent> containsList, String system) { 525 for (ValueSetExpansionContainsComponent contains : containsList) { 526 if (!contains.getSystem().equals(system) || (contains.hasContains() && !checkSystem(contains.getContains(), system))) { 527 return false; 528 } 529 } 530 return true; 531 } 532 533 private ConceptDefinitionComponent findCodeInConcept(ConceptDefinitionComponent concept, String code) { 534 if (code.equals(concept.getCode())) { 535 return concept; 536 } 537 ConceptDefinitionComponent cc = findCodeInConcept(concept.getConcept(), code); 538 if (cc != null) { 539 return cc; 540 } 541 if (concept.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) { 542 List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) concept.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK); 543 for (ConceptDefinitionComponent c : children) { 544 cc = findCodeInConcept(c, code); 545 if (cc != null) { 546 return cc; 547 } 548 } 549 } 550 return null; 551 } 552 553 private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code) { 554 for (ConceptDefinitionComponent cc : concept) { 555 if (code.equals(cc.getCode())) { 556 return cc; 557 } 558 ConceptDefinitionComponent c = findCodeInConcept(cc, code); 559 if (c != null) { 560 return c; 561 } 562 } 563 return null; 564 } 565 566 567 private String systemForCodeInValueSet(String code, List<String> problems) { 568 Set<String> sys = new HashSet<>(); 569 if (!scanForCodeInValueSet(code, sys, problems)) { 570 return null; 571 } 572 if (sys.size() != 1) { 573 problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_MULTIPLE_MATCHES, sys.toString())); 574 return null; 575 } else { 576 return sys.iterator().next(); 577 } 578 } 579 580 private boolean scanForCodeInValueSet(String code, Set<String> sys, List<String> problems) { 581 if (valueset.hasCompose()) { 582 // ignore excludes - they can't make any difference 583 if (!valueset.getCompose().hasInclude() && !valueset.getExpansion().hasContains()) { 584 problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_NO_INCLUDES_OR_EXPANSION, valueset.getVersionedUrl())); 585 } 586 587 int i = 0; 588 for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) { 589 if (vsi.hasValueSet()) { 590 for (CanonicalType u : vsi.getValueSet()) { 591 if (!checkForCodeInValueSet(code, u.getValue(), sys, problems)) { 592 return false; 593 } 594 } 595 } else if (!vsi.hasSystem()) { 596 problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_NO_SYSTEM, valueset.getVersionedUrl(), i)); 597 return false; 598 } 599 if (vsi.hasSystem()) { 600 if (vsi.hasFilter()) { 601 problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_NO_SYSTEM, valueset.getVersionedUrl(), i, vsi.getSystem())); 602 return false; 603 } 604 CodeSystem cs = resolveCodeSystem(vsi.getSystem(), vsi.getVersion()); 605 if (cs != null && cs.getContent() == CodeSystemContentMode.COMPLETE) { 606 607 if (vsi.hasConcept()) { 608 for (ConceptReferenceComponent cc : vsi.getConcept()) { 609 boolean match = cs.getCaseSensitive() ? cc.getCode().equals(code) : cc.getCode().equalsIgnoreCase(code); 610 if (match) { 611 sys.add(vsi.getSystem()); 612 } 613 } 614 } else { 615 ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code); 616 if (cc != null) { 617 sys.add(vsi.getSystem()); 618 } 619 } 620 } else if (vsi.hasConcept()) { 621 for (ConceptReferenceComponent cc : vsi.getConcept()) { 622 boolean match = cc.getCode().equals(code); 623 if (match) { 624 sys.add(vsi.getSystem()); 625 } 626 } 627 } else { 628 // we'll try to expand this one then 629 ValueSetExpansionOutcome vse = context.expandVS(vsi, false, false); 630 if (vse.isOk()) { 631 if (!checkSystems(vse.getValueset().getExpansion().getContains(), code, sys, problems)) { 632 return false; 633 } 634 } else { 635 problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_UNKNOWN_SYSTEM, valueset.getVersionedUrl(), i, vsi.getSystem(), vse.getAllErrors().toString())); 636 return false; 637 } 638 } 639 } 640 i++; 641 } 642 } else if (valueset.hasExpansion()) { 643 // Retrieve a list of all systems associated with this code in the expansion 644 if (!checkSystems(valueset.getExpansion().getContains(), code, sys, problems)) { 645 return false; 646 } 647 } 648 return true; 649 } 650 651 private boolean checkForCodeInValueSet(String code, String uri, Set<String> sys, List<String> problems) { 652 ValueSetCheckerSimple vs = getVs(uri); 653 return vs.scanForCodeInValueSet(code, sys, problems); 654 } 655 656 /* 657 * Recursively go through all codes in the expansion and for any coding that matches the specified code, add the system for that coding 658 * to the passed list. 659 */ 660 private boolean checkSystems(List<ValueSetExpansionContainsComponent> contains, String code, Set<String> systems, List<String> problems) { 661 for (ValueSetExpansionContainsComponent c: contains) { 662 if (c.getCode().equals(code)) { 663 systems.add(c.getSystem()); 664 } 665 if (c.hasContains()) 666 checkSystems(c.getContains(), code, systems, problems); 667 } 668 return true; 669 } 670 671 @Override 672 public Boolean codeInValueSet(String system, String version, String code, ValidationProcessInfo info) throws FHIRException { 673 if (valueset == null) { 674 return false; 675 } 676 Boolean result = false; 677 678 if (valueset.hasExpansion()) { 679 return checkExpansion(new Coding(system, code, null)); 680 } else if (valueset.hasCompose()) { 681 int i = 0; 682 for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) { 683 Boolean ok = inComponent(vsi, i, system, version, code, valueset.getCompose().getInclude().size() == 1, info); 684 i++; 685 if (ok == null && result == false) { 686 result = null; 687 } else if (ok) { 688 result = true; 689 break; 690 } 691 } 692 i = valueset.getCompose().getInclude().size(); 693 for (ConceptSetComponent vsi : valueset.getCompose().getExclude()) { 694 Boolean nok = inComponent(vsi, i, system, version, code, valueset.getCompose().getInclude().size() == 1, info); 695 i++; 696 if (nok == null && result == false) { 697 result = null; 698 } else if (nok != null && nok) { 699 result = false; 700 } 701 } 702 } 703 704 return result; 705 } 706 707 private Boolean inComponent(ConceptSetComponent vsi, int vsiIndex, String system, String version, String code, boolean only, ValidationProcessInfo info) throws FHIRException { 708 boolean ok = true; 709 710 if (vsi.hasValueSet()) { 711 if (isValueSetUnionImports()) { 712 ok = false; 713 for (UriType uri : vsi.getValueSet()) { 714 if (inImport(uri.getValue(), system, version, code)) { 715 return true; 716 } 717 } 718 } else { 719 ok = inImport(vsi.getValueSet().get(0).getValue(), system, version, code); 720 for (int i = 1; i < vsi.getValueSet().size(); i++) { 721 UriType uri = vsi.getValueSet().get(i); 722 ok = ok && inImport(uri.getValue(), system, version, code); 723 } 724 } 725 } 726 727 if (!vsi.hasSystem() || !ok) { 728 return ok; 729 } 730 731 if (only && system == null) { 732 // whether we know the system or not, we'll accept the stated codes at face value 733 for (ConceptReferenceComponent cc : vsi.getConcept()) { 734 if (cc.getCode().equals(code)) { 735 return true; 736 } 737 } 738 } 739 740 if (!system.equals(vsi.getSystem())) 741 return false; 742 // ok, we need the code system 743 CodeSystem cs = resolveCodeSystem(system, version); 744 if (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)) { 745 // make up a transient value set with 746 ValueSet vs = new ValueSet(); 747 vs.setStatus(PublicationStatus.ACTIVE); 748 vs.setUrl(valueset.getUrl()+"--"+vsiIndex); 749 vs.setVersion(valueset.getVersion()); 750 vs.getCompose().addInclude(vsi); 751 ValidationResult res = context.validateCode(options.noClient(), new Coding(system, code, null), vs); 752 if (res.getErrorClass() == TerminologyServiceErrorClass.UNKNOWN || res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED || res.getErrorClass() == TerminologyServiceErrorClass.VALUESET_UNSUPPORTED) { 753 if (info != null && res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) { 754 // server didn't know the code system either - we'll take it face value 755 info.getWarnings().add(context.formatMessage(I18nConstants.TERMINOLOGY_TX_SYSTEM_NOTKNOWN, system)); 756 for (ConceptReferenceComponent cc : vsi.getConcept()) { 757 if (cc.getCode().equals(code)) { 758 return true; 759 } 760 } 761 info.setErr(TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED); 762 } 763 return null; 764 } 765 if (res.getErrorClass() == TerminologyServiceErrorClass.NOSERVICE) { 766 throw new NoTerminologyServiceException(); 767 } 768 return res.isOk(); 769 } else { 770 if (vsi.hasFilter()) { 771 ok = true; 772 for (ConceptSetFilterComponent f : vsi.getFilter()) { 773 if (!codeInFilter(cs, system, f, code)) { 774 return false; 775 } 776 } 777 } 778 779 List<ConceptDefinitionComponent> list = cs.getConcept(); 780 ok = validateCodeInConceptList(code, cs, list); 781 if (ok && vsi.hasConcept()) { 782 for (ConceptReferenceComponent cc : vsi.getConcept()) { 783 if (cc.getCode().equals(code)) { 784 return true; 785 } 786 } 787 return false; 788 } else { 789 return ok; 790 } 791 } 792 } 793 794 protected boolean isValueSetUnionImports() { 795 PackageInformation p = (PackageInformation) valueset.getSourcePackage(); 796 if (p != null) { 797 return p.getDate().before(new GregorianCalendar(2022, Calendar.MARCH, 31).getTime()); 798 } else { 799 return false; 800 } 801 } 802 803 private boolean codeInFilter(CodeSystem cs, String system, ConceptSetFilterComponent f, String code) throws FHIRException { 804 if ("concept".equals(f.getProperty())) 805 return codeInConceptFilter(cs, f, code); 806 else { 807 System.out.println("todo: handle filters with property = "+f.getProperty()); 808 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__FILTER_WITH_PROPERTY__, cs.getUrl(), f.getProperty())); 809 } 810 } 811 812 private boolean codeInConceptFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) throws FHIRException { 813 switch (f.getOp()) { 814 case ISA: return codeInConceptIsAFilter(cs, f, code, false); 815 case ISNOTA: return !codeInConceptIsAFilter(cs, f, code, false); 816 case DESCENDENTOF: return codeInConceptIsAFilter(cs, f, code, true); 817 default: 818 System.out.println("todo: handle concept filters with op = "+f.getOp()); 819 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__CONCEPT_FILTER_WITH_OP__, cs.getUrl(), f.getOp())); 820 } 821 } 822 823 private boolean codeInConceptIsAFilter(CodeSystem cs, ConceptSetFilterComponent f, String code, boolean rootOnly) { 824 if (!rootOnly && code.equals(f.getProperty())) { 825 return true; 826 } 827 ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), f.getValue()); 828 if (cc == null) { 829 return false; 830 } 831 cc = findCodeInConcept(cc, code); 832 return cc != null; 833 } 834 835 public boolean validateCodeInConceptList(String code, CodeSystem def, List<ConceptDefinitionComponent> list) { 836 if (def.getCaseSensitive()) { 837 for (ConceptDefinitionComponent cc : list) { 838 if (cc.getCode().equals(code)) { 839 return true; 840 } 841 if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept())) { 842 return true; 843 } 844 } 845 } else { 846 for (ConceptDefinitionComponent cc : list) { 847 if (cc.getCode().equalsIgnoreCase(code)) { 848 return true; 849 } 850 if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept())) { 851 return true; 852 } 853 } 854 } 855 return false; 856 } 857 858 private ValueSetCheckerSimple getVs(String url) { 859 if (inner.containsKey(url)) { 860 return inner.get(url); 861 } 862 ValueSet vs = context.fetchResource(ValueSet.class, url, valueset); 863 ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, context, localContext); 864 inner.put(url, vsc); 865 return vsc; 866 } 867 868 private boolean inImport(String uri, String system, String version, String code) throws FHIRException { 869 ValueSetCheckerSimple vs = getVs(uri); 870 if (vs == null) { 871 return false; 872 } else { 873 Boolean ok = vs.codeInValueSet(system, version, code, null); 874 return ok != null && ok; 875 } 876 } 877 878}