001package org.hl7.fhir.r5.terminologies.validation; 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 032import java.util.ArrayList; 033import java.util.Arrays; 034import java.util.Calendar; 035import java.util.GregorianCalendar; 036import java.util.HashMap; 037import java.util.HashSet; 038import java.util.List; 039import java.util.Map; 040import java.util.Set; 041 042 043import org.hl7.fhir.exceptions.FHIRException; 044import org.hl7.fhir.exceptions.NoTerminologyServiceException; 045import org.hl7.fhir.r5.context.ContextUtilities; 046import org.hl7.fhir.r5.context.IWorkerContext; 047import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult; 048import org.hl7.fhir.r5.elementmodel.LanguageUtils; 049import org.hl7.fhir.r5.extensions.ExtensionConstants; 050import org.hl7.fhir.r5.model.CanonicalType; 051import org.hl7.fhir.r5.model.CodeSystem; 052import org.hl7.fhir.r5.model.CodeType; 053import org.hl7.fhir.r5.model.Enumerations.CodeSystemContentMode; 054import org.hl7.fhir.r5.model.Enumerations.FilterOperator; 055import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; 056import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionDesignationComponent; 057import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent; 058import org.hl7.fhir.r5.model.CodeableConcept; 059import org.hl7.fhir.r5.model.Coding; 060import org.hl7.fhir.r5.model.DataType; 061import org.hl7.fhir.r5.model.Extension; 062import org.hl7.fhir.r5.model.Enumerations.PublicationStatus; 063import org.hl7.fhir.r5.model.OperationOutcome.IssueType; 064import org.hl7.fhir.r5.model.OperationOutcome.OperationOutcomeIssueComponent; 065import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent; 066import org.hl7.fhir.r5.model.PackageInformation; 067import org.hl7.fhir.r5.model.Parameters; 068import org.hl7.fhir.r5.model.TerminologyCapabilities.TerminologyCapabilitiesCodeSystemComponent; 069import org.hl7.fhir.r5.model.TerminologyCapabilities; 070import org.hl7.fhir.r5.model.Transport.ParameterComponent; 071import org.hl7.fhir.r5.model.UriType; 072import org.hl7.fhir.r5.model.ValueSet; 073import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent; 074import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceDesignationComponent; 075import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; 076import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent; 077import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; 078import org.hl7.fhir.r5.terminologies.CodeSystemUtilities; 079import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome; 080import org.hl7.fhir.r5.terminologies.providers.CodeSystemProvider; 081import org.hl7.fhir.r5.terminologies.providers.SpecialCodeSystem; 082import org.hl7.fhir.r5.terminologies.providers.URICodeSystem; 083import org.hl7.fhir.r5.terminologies.utilities.TerminologyOperationContext; 084import org.hl7.fhir.r5.terminologies.utilities.TerminologyOperationContext.TerminologyServiceProtectionException; 085import org.hl7.fhir.r5.terminologies.utilities.TerminologyServiceErrorClass; 086import org.hl7.fhir.r5.terminologies.utilities.ValueSetProcessBase; 087import org.hl7.fhir.r5.utils.ToolingExtensions; 088import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier; 089import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier.ValidationContextResourceProxy; 090import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 091import org.hl7.fhir.utilities.Utilities; 092import org.hl7.fhir.utilities.VersionUtilities; 093import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader; 094import org.hl7.fhir.utilities.i18n.I18nConstants; 095import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 096import org.hl7.fhir.utilities.validation.ValidationOptions; 097import org.hl7.fhir.utilities.validation.ValidationOptions.ValueSetMode; 098 099import com.google.j2objc.annotations.ReflectionSupport.Level; 100 101public class ValueSetValidator extends ValueSetProcessBase { 102 103 private ValueSet valueset; 104 private Map<String, ValueSetValidator> inner = new HashMap<>(); 105 private ValidationOptions options; 106 private ValidationContextCarrier localContext; 107 private List<CodeSystem> localSystems = new ArrayList<>(); 108 protected Parameters expansionProfile; 109 private TerminologyCapabilities txCaps; 110 private Set<String> unknownSystems; 111 private boolean throwToServer; 112 113 public ValueSetValidator(IWorkerContext context, TerminologyOperationContext opContext, ValidationOptions options, ValueSet source, Parameters expansionProfile, TerminologyCapabilities txCaps) { 114 super(context, opContext); 115 this.valueset = source; 116 this.options = options; 117 this.expansionProfile = expansionProfile; 118 this.txCaps = txCaps; 119 analyseValueSet(); 120 } 121 122 public ValueSetValidator(IWorkerContext context, TerminologyOperationContext opContext, ValidationOptions options, ValueSet source, ValidationContextCarrier ctxt, Parameters expansionProfile, TerminologyCapabilities txCaps) { 123 super(context, opContext); 124 this.valueset = source; 125 this.options = options.copy(); 126 this.options.setEnglishOk(true); 127 this.localContext = ctxt; 128 this.expansionProfile = expansionProfile; 129 this.txCaps = txCaps; 130 analyseValueSet(); 131 } 132 133 public Set<String> getUnknownSystems() { 134 return unknownSystems; 135 } 136 137 public void setUnknownSystems(Set<String> unknownSystems) { 138 this.unknownSystems = unknownSystems; 139 } 140 141 public boolean isThrowToServer() { 142 return throwToServer; 143 } 144 145 public void setThrowToServer(boolean throwToServer) { 146 this.throwToServer = throwToServer; 147 } 148 149 private void analyseValueSet() { 150 if (valueset != null) { 151 opContext.seeContext(valueset.getVersionedUrl()); 152 for (Extension s : valueset.getExtensionsByUrl(ExtensionConstants.EXT_VSSUPPLEMENT)) { 153 requiredSupplements.add(s.getValue().primitiveValue()); 154 } 155 } 156 157 altCodeParams.seeParameters(expansionProfile); 158 altCodeParams.seeValueSet(valueset); 159 if (localContext != null) { 160 if (valueset != null) { 161 for (ConceptSetComponent i : valueset.getCompose().getInclude()) { 162 analyseComponent(i); 163 } 164 for (ConceptSetComponent i : valueset.getCompose().getExclude()) { 165 analyseComponent(i); 166 } 167 } 168 } 169 } 170 171 private void analyseComponent(ConceptSetComponent i) { 172 opContext.deadCheck(); 173 if (i.getSystemElement().hasExtension(ToolingExtensions.EXT_VALUESET_SYSTEM)) { 174 String ref = i.getSystemElement().getExtensionString(ToolingExtensions.EXT_VALUESET_SYSTEM); 175 if (ref.startsWith("#")) { 176 String id = ref.substring(1); 177 for (ValidationContextResourceProxy t : localContext.getResources()) { 178 CodeSystem cs = (CodeSystem) t.loadContainedResource(id, CodeSystem.class); 179 if (cs != null) { 180 localSystems.add(cs); 181 } 182 } 183 } else { 184 throw new Error("Not done yet #2: "+ref); 185 } 186 } 187 } 188 189 public ValidationResult validateCode(CodeableConcept code) throws FHIRException { 190 return validateCode("CodeableConcept", code); 191 } 192 193 public ValidationResult validateCode(String path, CodeableConcept code) throws FHIRException { 194 opContext.deadCheck(); 195 checkValueSetOptions(); 196 197 // first, we validate the codings themselves 198 ValidationProcessInfo info = new ValidationProcessInfo(); 199 200 CodeableConcept vcc = new CodeableConcept(); 201 202 if (options.getValueSetMode() != ValueSetMode.CHECK_MEMERSHIP_ONLY) { 203 int i = 0; 204 for (Coding c : code.getCoding()) { 205 if (!c.hasSystem()) { 206 info.addIssue(makeIssue(IssueSeverity.WARNING, IssueType.UNKNOWN, path, context.formatMessage(I18nConstants.CODING_HAS_NO_SYSTEM__CANNOT_VALIDATE))); 207 } 208 VersionInfo vi = new VersionInfo(this); 209 checkExpansion(c, vi); 210 checkInclude(c, vi); 211 CodeSystem cs = resolveCodeSystem(c.getSystem(), vi.getVersion(c.getSystem(), c.getVersion())); 212 ValidationResult res = null; 213 if (cs == null || cs.getContent() != CodeSystemContentMode.COMPLETE) { 214 if (context.isNoTerminologyServer()) { 215 if (c.hasVersion()) { 216 String msg = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM_VERSION, c.getSystem(), c.getVersion() , resolveCodeSystemVersions(c.getSystem()).toString()); 217 unknownSystems.add(c.getSystem()+"|"+c.getVersion()); 218 res = new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path+".coding["+i+"].system", msg)).setUnknownSystems(unknownSystems); 219 } else { 220 String msg = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM, c.getSystem(), c.getVersion()); 221 unknownSystems.add(c.getSystem()); 222 res = new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path+".coding["+i+"].system", msg)).setUnknownSystems(unknownSystems); 223 } 224 } else { 225 res = context.validateCode(options.withNoClient(), c, null); 226 } 227 } else { 228 c.setUserData("cs", cs); 229 res = validateCode(path+".coding["+i+"]", c, cs, vcc, info); 230 } 231 info.getIssues().addAll(res.getIssues()); 232 i++; 233 } 234 } 235 Coding foundCoding = null; 236 String msg = null; 237 Boolean result = false; 238 if (valueset != null && options.getValueSetMode() != ValueSetMode.NO_MEMBERSHIP_CHECK) { 239 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(", "); 240 241 for (Coding c : code.getCoding()) { 242 b.append("'"+c.getSystem()+(c.hasVersion() ? "|"+c.getVersion() : "")+"#"+c.getCode()+"'"); 243 Boolean ok = codeInValueSet(path, c.getSystem(), c.getVersion(), c.getCode(), info); 244 if (ok == null && result != null && result == false) { 245 result = null; 246 } else if (ok != null && ok) { 247 result = true; 248 foundCoding = c; 249 if (options.getValueSetMode() == ValueSetMode.CHECK_MEMERSHIP_ONLY) { 250 vcc.addCoding().setSystem(c.getSystem()).setVersion(c.getVersion()).setCode(c.getCode()); 251 } 252 } 253 if (ok == null || !ok) { 254 vcc.removeCoding(c.getSystem(), c.getVersion(), c.getCode()); 255 } 256 } 257 if (result == null) { 258 msg = context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), b.toString()); 259 info.getIssues().addAll(makeIssue(IssueSeverity.WARNING, unknownSystems.isEmpty() ? IssueType.CODEINVALID : IssueType.NOTFOUND, path, msg)); 260 } else if (!result) { 261 msg = context.formatMessagePlural(code.getCoding().size(), I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), b.toString()); 262 info.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path, msg)); 263 } 264 } 265 if (vcc.hasCoding() && code.hasText()) { 266 vcc.setText(code.getText()); 267 } 268 if (!checkRequiredSupplements(info)) { 269 return new ValidationResult(IssueSeverity.ERROR, info.getIssues().get(info.getIssues().size()-1).getDetails().getText(), info.getIssues()); 270 } else if (info.hasErrors()) { 271 ValidationResult res = new ValidationResult(IssueSeverity.ERROR, info.summary(), info.getIssues()); 272 if (foundCoding != null) { 273 ConceptDefinitionComponent cd = new ConceptDefinitionComponent(foundCoding.getCode()); 274 cd.setDisplay(lookupDisplay(foundCoding)); 275 res.setDefinition(cd); 276 res.setSystem(foundCoding.getSystem()); 277 res.setVersion(foundCoding.hasVersion() ? foundCoding.getVersion() : ((CodeSystem) foundCoding.getUserData("cs")).getVersion()); 278 res.setDisplay(cd.getDisplay()); 279 } 280 res.setUnknownSystems(unknownSystems); 281 res.addCodeableConcept(vcc); 282 return res; 283 } else if (result == null) { 284 return new ValidationResult(IssueSeverity.WARNING, info.summary(), info.getIssues()); 285 } else if (foundCoding == null) { 286 return new ValidationResult(IssueSeverity.ERROR, "Internal Error that should not happen", makeIssue(IssueSeverity.FATAL, IssueType.EXCEPTION, path, "Internal Error that should not happen")); 287 } else if (info.getIssues().size() > 0) { 288 String disp = lookupDisplay(foundCoding); 289 ConceptDefinitionComponent cd = new ConceptDefinitionComponent(foundCoding.getCode()); 290 cd.setDisplay(disp); 291 return new ValidationResult(IssueSeverity.WARNING, info.summary(), foundCoding.getSystem(), getVersion(foundCoding), cd, disp, info.getIssues()).addCodeableConcept(vcc); 292 } else { 293 ConceptDefinitionComponent cd = new ConceptDefinitionComponent(foundCoding.getCode()); 294 cd.setDisplay(lookupDisplay(foundCoding)); 295 return new ValidationResult(foundCoding.getSystem(), getVersion(foundCoding), cd, getPreferredDisplay(cd, null)).addCodeableConcept(vcc); 296 } 297 } 298 299 private boolean checkRequiredSupplements(ValidationProcessInfo info) { 300 if (!requiredSupplements.isEmpty()) { 301 String msg= context.formatMessagePlural(requiredSupplements.size(), I18nConstants.VALUESET_SUPPLEMENT_MISSING, CommaSeparatedStringBuilder.build(requiredSupplements)); 302 throw new TerminologyServiceProtectionException(msg, TerminologyServiceErrorClass.BUSINESS_RULE, IssueType.NOTFOUND); 303 } 304 return requiredSupplements.isEmpty(); 305 } 306 307 private boolean valueSetDependsOn(String system, String version) { 308 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 309 if (system.equals(inc.getSystem()) && (version == null || inc.getVersion() == null || version.equals(inc.getVersion()))) { 310 return true; 311 } 312 } 313 return false; 314 } 315 316 private String getVersion(Coding c) { 317 if (c.hasVersion()) { 318 return c.getVersion(); 319 } else if (c.hasUserData("cs")) { 320 return ((CodeSystem) c.getUserData("cs")).getVersion(); 321 } else { 322 return null; 323 } 324 } 325 326 private String lookupDisplay(Coding c) { 327 CodeSystem cs = resolveCodeSystem(c.getSystem(), c.getVersion()); 328 if (cs != null) { 329 ConceptDefinitionComponent cd = CodeSystemUtilities.findCodeOrAltCode(cs.getConcept(), c.getCode(), null); 330 if (cd != null) { 331 return getPreferredDisplay(cd, cs); 332 } 333 } 334 return null; 335 } 336 337 public CodeSystem resolveCodeSystem(String system, String version) { 338 for (CodeSystem t : localSystems) { 339 if (t.getUrl().equals(system) && versionsMatch(version, t.getVersion())) { 340 return t; 341 } 342 } 343 CodeSystem cs = context.fetchSupplementedCodeSystem(system, version); 344 if (cs == null) { 345 cs = findSpecialCodeSystem(system, version); 346 } 347 return cs; 348 } 349 350 public List<String> resolveCodeSystemVersions(String system) { 351 List<String> res = new ArrayList<>(); 352 for (CodeSystem t : localSystems) { 353 if (t.getUrl().equals(system) && t.hasVersion()) { 354 res.add(t.getVersion()); 355 } 356 } 357 res.addAll(new ContextUtilities(context).fetchCodeSystemVersions(system)); 358 return res; 359 } 360 361 private boolean versionsMatch(String versionTest, String versionActual) { 362 return versionTest == null && VersionUtilities.versionsMatch(versionTest, versionActual); 363 } 364 365 public ValidationResult validateCode(Coding code) throws FHIRException { 366 return validateCode("Coding", code); 367 } 368 369 public ValidationResult validateCode(String path, Coding code) throws FHIRException { 370 opContext.deadCheck(); 371 checkValueSetOptions(); 372 373 String warningMessage = null; 374 // first, we validate the concept itself 375 376 ValidationResult res = null; 377 boolean inExpansion = false; 378 boolean inInclude = false; 379 List<OperationOutcomeIssueComponent> issues = new ArrayList<>(); 380 ValidationProcessInfo info = new ValidationProcessInfo(issues); 381 VersionInfo vi = new VersionInfo(this); 382 checkCanonical(issues, path, valueset, valueset); 383 384 String system = code.hasSystem() ? code.getSystem() : getValueSetSystemOrNull(); 385 if (options.getValueSetMode() != ValueSetMode.CHECK_MEMERSHIP_ONLY) { 386 if (system == null && !code.hasDisplay()) { // dealing with just a plain code (enum) 387 List<String> problems = new ArrayList<>(); 388 system = systemForCodeInValueSet(code.getCode(), problems); 389 if (system == null) { 390 if (problems.size() == 0) { 391 throw new Error("Unable to resolve systems but no reason why"); // this is an error in the java code 392 } else if (problems.size() == 1) { 393 return new ValidationResult(IssueSeverity.ERROR, problems.get(0), makeIssue(IssueSeverity.ERROR, IssueType.UNKNOWN, path, problems.get(0))); 394 } else { 395 ValidationResult vr = new ValidationResult(IssueSeverity.ERROR, problems.toString(), null); 396 for (String s : problems) { 397 vr.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.UNKNOWN, path, s)); 398 } 399 return vr; 400 } 401 } 402 } 403 if (!code.hasSystem()) { 404 if (options.isGuessSystem() && system == null && Utilities.isAbsoluteUrl(code.getCode())) { 405 system = "urn:ietf:rfc:3986"; // this arises when using URIs bound to value sets 406 } 407 code.setSystem(system); 408 } 409 inExpansion = checkExpansion(code, vi); 410 inInclude = checkInclude(code, vi); 411 String wv = vi.getVersion(system, code.getVersion()); 412 CodeSystem cs = resolveCodeSystem(system, wv); 413 if (cs == null) { 414 if (wv == null) { 415 warningMessage = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM, system); 416 unknownSystems.add(system); 417 } else { 418 warningMessage = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM_VERSION, system, wv, resolveCodeSystemVersions(system).toString()); 419 unknownSystems.add(system+"|"+wv); 420 } 421 if (!inExpansion) { 422 if (valueset != null && valueset.hasExpansion()) { 423 String msg = context.formatMessage(I18nConstants.CODESYSTEM_CS_UNK_EXPANSION, 424 valueset.getUrl(), 425 code.getSystem(), 426 code.getCode().toString()); 427 issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path, msg)); 428 throw new VSCheckerException(msg, issues); 429 } else { 430 issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path+".system", warningMessage)); 431 res = new ValidationResult(IssueSeverity.WARNING, warningMessage, issues); 432 if (valueset == null) { 433 throw new VSCheckerException(warningMessage, issues); 434 } else { 435// String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl(), code.toString()); 436// issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path, msg)); 437 // we don't do this yet 438 // throw new VSCheckerException(warningMessage, issues); 439 } 440 } 441 } 442 } else { 443 checkCanonical(issues, path, cs, valueset); 444 } 445 if (cs != null && cs.hasSupplements()) { 446 String msg = context.formatMessage(I18nConstants.CODESYSTEM_CS_NO_SUPPLEMENT, cs.getUrl()); 447 return new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path, msg)); 448 } 449 if (cs!=null && cs.getContent() != CodeSystemContentMode.COMPLETE) { 450 warningMessage = "Resolved system "+system+(cs.hasVersion() ? " (v"+cs.getVersion()+")" : "")+", but the definition is not complete"; 451 if (!inExpansion && cs.getContent() != CodeSystemContentMode.FRAGMENT) { // we're going to give it a go if it's a fragment 452 throw new VSCheckerException(warningMessage, null, true); 453 } 454 } 455 456 if (cs != null /*&& (cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT)*/) { 457 if (!(cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT)) { 458 if (inInclude) { 459 ConceptReferenceComponent cc = findInInclude(code); 460 if (cc != null) { 461 // we'll take it on faith 462 String disp = getPreferredDisplay(cc); 463 res = new ValidationResult(system, cs.getVersion(), new ConceptDefinitionComponent().setCode(cc.getCode()).setDisplay(disp), disp); 464 res.addToMessage("Resolved system "+system+", but the definition is not complete, so assuming value set include is correct"); 465 return res; 466 } 467 } 468 // we can't validate that here. 469 throw new FHIRException("Unable to evaluate based on empty code system"); 470 } 471 res = validateCode(path, code, cs, null, info); 472 res.setIssues(issues); 473 } else if (cs == null && valueset.hasExpansion() && inExpansion) { 474 // we just take the value set as face value then 475 res = new ValidationResult(system, wv, new ConceptDefinitionComponent().setCode(code.getCode()).setDisplay(code.getDisplay()), code.getDisplay()); 476 if (!preferServerSide(system)) { 477 res.addToMessage("Code System unknown, so assuming value set expansion is correct ("+warningMessage+")"); 478 } 479 } else { 480 // well, we didn't find a code system - try the expansion? 481 // disabled waiting for discussion 482 if (throwToServer) { 483 throw new FHIRException("No; try the server"); 484 } 485 } 486 } else { 487 inExpansion = checkExpansion(code, vi); 488 inInclude = checkInclude(code, vi); 489 } 490 String wv = vi.getVersion(system, code.getVersion()); 491 if (!checkRequiredSupplements(info)) { 492 return new ValidationResult(IssueSeverity.ERROR, issues.get(issues.size()-1).getDetails().getText(), issues); 493 } 494 495 496 // then, if we have a value set, we check it's in the value set 497 if (valueset != null && options.getValueSetMode() != ValueSetMode.NO_MEMBERSHIP_CHECK) { 498 if ((res==null || res.isOk())) { 499 Boolean ok = codeInValueSet(path, system, wv, code.getCode(), info); 500 if (ok == null || !ok) { 501 if (res == null) { 502 res = new ValidationResult((IssueSeverity) null, null, info.getIssues()); 503 } 504 if (info.getErr() != null) { 505 res.setErrorClass(info.getErr()); 506 } 507 if (ok == null) { 508 String m = "Unable to check whether the code is in the value set "+valueset.getVersionedUrl(); 509 res.addToMessage(m); 510 res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.NOTFOUND, path, m)); 511 res.setUnknownSystems(unknownSystems); 512 res.setSeverity(IssueSeverity.ERROR); // back patching for display logic issue 513 res.setErrorClass(TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED); 514 } else if (!inExpansion && !inInclude) { 515// if (!info.getIssues().isEmpty()) { 516// res.setMessage("Not in value set "+valueset.getUrl()+": "+info.summary()).setSeverity(IssueSeverity.ERROR); 517// res.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path, res.getMessage())); 518// } else 519// { 520 String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), "'"+code.toString()+"'"); 521 res.addToMessage(msg).setSeverity(IssueSeverity.ERROR); 522 res.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path, msg)); 523 res.setDefinition(null); 524 res.setSystem(null); 525 res.setDisplay(null); 526 res.setUnknownSystems(unknownSystems); 527// } 528 } else if (warningMessage!=null) { 529 String msg = context.formatMessage(I18nConstants.CODE_FOUND_IN_EXPANSION_HOWEVER_, warningMessage); 530 res = new ValidationResult(IssueSeverity.WARNING, msg, makeIssue(IssueSeverity.WARNING, IssueType.EXCEPTION, path, msg)); 531 } else if (inExpansion) { 532 res.setMessage("Code found in expansion, however: " + res.getMessage()); 533 res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.EXCEPTION, path, res.getMessage())); 534 } else if (inInclude) { 535 res.setMessage("Code found in include, however: " + res.getMessage()); 536 res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.EXCEPTION, path, res.getMessage())); 537 } 538 } else if (res == null) { 539 res = new ValidationResult(system, wv, null, null); 540 } 541 } else if ((res != null && !res.isOk())) { 542 String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), "'"+code.toString()+"'"); 543 res.setMessage(res.getMessage()+"; "+msg); 544 res.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path, msg)); 545 } 546 } 547 if (res != null && res.getSeverity() == IssueSeverity.INFORMATION) { 548 res.setSeverity(IssueSeverity.ERROR); // back patching for display logic issue 549 } 550 return res; 551 } 552 553 private void checkValueSetOptions() { 554 if (valueset != null) { 555 for (Extension ext : valueset.getCompose().getExtensionsByUrl("http://hl7.org/fhir/tools/StructureDefinion/valueset-expansion-param")) { 556 var name = ext.getExtensionString("name"); 557 var value = ext.getExtensionByUrl("value").getValue(); 558 if ("displayLanguage".equals(name)) { 559 options.setLanguages(value.primitiveValue()); 560 } 561 } 562 if (!options.hasLanguages() && valueset.hasLanguage()) { 563 options.addLanguage(valueset.getLanguage()); 564 } 565 } 566 } 567 568 private static final Set<String> SERVER_SIDE_LIST = new HashSet<>(Arrays.asList("http://fdasis.nlm.nih.gov", "http://hl7.org/fhir/sid/ndc", "http://loinc.org", "http://snomed.info/sct", "http://unitsofmeasure.org", 569 "http://unstats.un.org/unsd/methods/m49/m49.htm", "http://varnomen.hgvs.org", "http://www.nlm.nih.gov/research/umls/rxnorm", "https://www.usps.com/", 570 "urn:ietf:bcp:13","urn:ietf:bcp:47","urn:ietf:rfc:3986", "urn:iso:std:iso:3166","urn:iso:std:iso:4217", "urn:oid:1.2.36.1.2001.1005.17")); 571 572 private boolean preferServerSide(String system) { 573 if (SERVER_SIDE_LIST.contains(system)) { 574 return true; 575 } 576 577 if (txCaps != null) { 578 for (TerminologyCapabilitiesCodeSystemComponent tccs : txCaps.getCodeSystem()) { 579 if (system.equals(tccs.getUri())) { 580 return true; 581 } 582 } 583 } 584 return false; 585 } 586 587 private boolean checkInclude(Coding code, VersionInfo vi) { 588 if (valueset == null || code.getSystem() == null || code.getCode() == null) { 589 return false; 590 } 591 for (ConceptSetComponent inc : valueset.getCompose().getExclude()) { 592 if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) { 593 for (ConceptReferenceComponent cc : inc.getConcept()) { 594 if (cc.hasCode() && cc.getCode().equals(code.getCode())) { 595 return false; 596 } 597 } 598 } 599 } 600 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 601 if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) { 602 vi.setComposeVersion(inc.getVersion()); 603 for (ConceptReferenceComponent cc : inc.getConcept()) { 604 if (cc.hasCode() && cc.getCode().equals(code.getCode())) { 605 return true; 606 } 607 } 608 } 609 } 610 return false; 611 } 612 613 private ConceptReferenceComponent findInInclude(Coding code) { 614 if (valueset == null || code.getSystem() == null || code.getCode() == null) { 615 return null; 616 } 617 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 618 if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) { 619 for (ConceptReferenceComponent cc : inc.getConcept()) { 620 if (cc.hasCode() && cc.getCode().equals(code.getCode())) { 621 return cc; 622 } 623 } 624 } 625 } 626 return null; 627 } 628 629 private CodeSystem findSpecialCodeSystem(String system, String version) { 630 if ("urn:ietf:rfc:3986".equals(system)) { 631 CodeSystem cs = new CodeSystem(); 632 cs.setUrl(system); 633 cs.setUserData("tx.cs.special", new URICodeSystem()); 634 cs.setContent(CodeSystemContentMode.COMPLETE); 635 return cs; 636 } 637 return null; 638 } 639 640 private ValidationResult findCodeInExpansion(Coding code) { 641 if (valueset==null || !valueset.hasExpansion()) 642 return null; 643 return findCodeInExpansion(code, valueset.getExpansion().getContains()); 644 } 645 646 private ValidationResult findCodeInExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) { 647 for (ValueSetExpansionContainsComponent containsComponent: contains) { 648 opContext.deadCheck(); 649 if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) { 650 ConceptDefinitionComponent ccd = new ConceptDefinitionComponent(); 651 ccd.setCode(containsComponent.getCode()); 652 ccd.setDisplay(containsComponent.getDisplay()); 653 ValidationResult res = new ValidationResult(code.getSystem(), code.hasVersion() ? code.getVersion() : containsComponent.getVersion(), ccd, getPreferredDisplay(ccd, null)); 654 return res; 655 } 656 if (containsComponent.hasContains()) { 657 ValidationResult res = findCodeInExpansion(code, containsComponent.getContains()); 658 if (res != null) { 659 return res; 660 } 661 } 662 } 663 return null; 664 } 665 666 private boolean checkExpansion(Coding code, VersionInfo vi) { 667 if (valueset==null || !valueset.hasExpansion()) { 668 return false; 669 } 670 return checkExpansion(code, valueset.getExpansion().getContains(), vi); 671 } 672 673 private boolean checkExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains, VersionInfo vi) { 674 for (ValueSetExpansionContainsComponent containsComponent: contains) { 675 opContext.deadCheck(); 676 if (containsComponent.hasSystem() && containsComponent.hasCode() && containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) { 677 vi.setExpansionVersion(containsComponent.getVersion()); 678 return true; 679 } 680 if (containsComponent.hasContains() && checkExpansion(code, containsComponent.getContains(), vi)) { 681 return true; 682 } 683 } 684 return false; 685 } 686 687 private ValidationResult validateCode(String path, Coding code, CodeSystem cs, CodeableConcept vcc, ValidationProcessInfo info) { 688 ConceptDefinitionComponent cc = cs.hasUserData("tx.cs.special") ? ((SpecialCodeSystem) cs.getUserData("tx.cs.special")).findConcept(code) : findCodeInConcept(cs.getConcept(), code.getCode(), allAltCodes); 689 if (cc == null) { 690 if (cs.getContent() == CodeSystemContentMode.FRAGMENT) { 691 String msg = context.formatMessage(I18nConstants.UNKNOWN_CODE__IN_FRAGMENT, code.getCode(), cs.getUrl()); 692 return new ValidationResult(IssueSeverity.WARNING, msg, makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path+".code", msg)); 693 } else { 694 String msg = context.formatMessage(I18nConstants.UNKNOWN_CODE__IN_, code.getCode(), cs.getUrl()); 695 return new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path+".code", msg)); 696 } 697 } 698 Coding vc = new Coding().setCode(cc.getCode()).setSystem(cs.getUrl()).setVersion(cs.getVersion()).setDisplay(getPreferredDisplay(cc, cs)); 699 if (vcc != null) { 700 vcc.addCoding(vc); 701 } 702 703 boolean inactive = (CodeSystemUtilities.isInactive(cs, cc)); 704 String status = inactive ? (CodeSystemUtilities.getStatus(cs, cc)) : null; 705 706 boolean ws = false; 707 if (code.getDisplay() == null) { 708 return new ValidationResult(code.getSystem(), cs.getVersion(), cc, vc.getDisplay()).setStatus(inactive, status); 709 } 710 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(", ", " or "); 711 if (cc.hasDisplay() && isOkLanguage(cs.getLanguage())) { 712 b.append("'"+cc.getDisplay()+"'"+(cs.hasLanguage() ? " ("+cs.getLanguage()+")" : "")); 713 if (code.getDisplay().equalsIgnoreCase(cc.getDisplay())) { 714 return new ValidationResult(code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status); 715 } else if (Utilities.normalize(code.getDisplay()).equals(Utilities.normalize(cc.getDisplay()))) { 716 ws = true; 717 } 718 } 719 720 for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) { 721 opContext.deadCheck(); 722 if (isOkLanguage(ds.getLanguage())) { 723 b.append("'"+ds.getValue()+"'"); 724 if (code.getDisplay().equalsIgnoreCase(ds.getValue())) { 725 return new ValidationResult(code.getSystem(),cs.getVersion(), cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status); 726 } 727 if (Utilities.normalize(code.getDisplay()).equalsIgnoreCase(Utilities.normalize(ds.getValue()))) { 728 ws = true; 729 } 730 } 731 } 732 // also check to see if the value set has another display 733 if (options.isUseValueSetDisplays()) { 734 ConceptReferencePair vs = findValueSetRef(code.getSystem(), code.getCode()); 735 if (vs != null && (vs.getCc().hasDisplay() ||vs.getCc().hasDesignation())) { 736 if (vs.getCc().hasDisplay() && isOkLanguage(vs.getValueset().getLanguage())) { 737 b.append("'"+vs.getCc().getDisplay()+"'"); 738 if (code.getDisplay().equalsIgnoreCase(vs.getCc().getDisplay())) { 739 return new ValidationResult(code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status); 740 } 741 } 742 for (ConceptReferenceDesignationComponent ds : vs.getCc().getDesignation()) { 743 opContext.deadCheck(); 744 if (isOkLanguage(ds.getLanguage())) { 745 b.append("'"+ds.getValue()+"'"); 746 if (code.getDisplay().equalsIgnoreCase(ds.getValue())) { 747 return new ValidationResult(code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status); 748 } 749 } 750 } 751 } 752 } 753 if (b.count() == 0) { 754 String msg = context.formatMessagePlural(options.getLanguages().getLangs().size(), I18nConstants.NO_VALID_DISPLAY_FOUND, code.getSystem(), code.getCode(), code.getDisplay(), options.langSummary()); 755 return new ValidationResult(IssueSeverity.WARNING, msg, code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs), makeIssue(IssueSeverity.WARNING, IssueType.INVALID, path+".display", msg)).setStatus(inactive, status); 756 } else { 757 String msg = context.formatMessagePlural(b.count(), ws ? I18nConstants.DISPLAY_NAME_WS_FOR__SHOULD_BE_ONE_OF__INSTEAD_OF : I18nConstants.DISPLAY_NAME_FOR__SHOULD_BE_ONE_OF__INSTEAD_OF, code.getSystem(), code.getCode(), b.toString(), code.getDisplay(), options.langSummary()); 758 return new ValidationResult(dispWarningStatus(), msg, code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs), makeIssue(dispWarning(), IssueType.INVALID, path+".display", msg)).setStatus(inactive, status); 759 } 760 } 761 762 private IssueSeverity dispWarning() { 763 return options.isDisplayWarningMode() ? IssueSeverity.WARNING : IssueSeverity.ERROR; 764 } 765 766 private IssueSeverity dispWarningStatus() { 767 return options.isDisplayWarningMode() ? IssueSeverity.WARNING : IssueSeverity.INFORMATION; // information -> error later 768 } 769 770 private boolean isOkLanguage(String language) { 771 if (!options.hasLanguages()) { 772 return true; 773 } 774 if (LanguageUtils.langsMatch(options.getLanguages(), language)) { 775 return true; 776 } 777 if (language == null && (options.langSummary().contains("en") || options.langSummary().contains("en-US") || options.isEnglishOk())) { 778 return true; 779 } 780 return false; 781 } 782 783 private ConceptReferencePair findValueSetRef(String system, String code) { 784 if (valueset == null) 785 return null; 786 // if it has an expansion 787 for (ValueSetExpansionContainsComponent exp : valueset.getExpansion().getContains()) { 788 opContext.deadCheck(); 789 if (system.equals(exp.getSystem()) && code.equals(exp.getCode())) { 790 ConceptReferenceComponent cc = new ConceptReferenceComponent(); 791 cc.setDisplay(exp.getDisplay()); 792 cc.setDesignation(exp.getDesignation()); 793 return new ConceptReferencePair(valueset, cc); 794 } 795 } 796 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 797 if (system.equals(inc.getSystem())) { 798 for (ConceptReferenceComponent cc : inc.getConcept()) { 799 if (cc.getCode().equals(code)) { 800 return new ConceptReferencePair(valueset, cc); 801 } 802 } 803 } 804 for (CanonicalType url : inc.getValueSet()) { 805 ConceptReferencePair cc = getVs(url.asStringValue()).findValueSetRef(system, code); 806 if (cc != null) { 807 return cc; 808 } 809 } 810 } 811 return null; 812 } 813 814 private String gen(Coding code) { 815 if (code.hasSystem()) { 816 return code.getSystem()+"#"+code.getCode(); 817 } else { 818 return null; 819 } 820 } 821 822 823 private String getValueSetSystemOrNull() throws FHIRException { 824 if (valueset == null) { 825 return null; 826 } 827 if (valueset.getCompose().getInclude().size() == 0) { 828 if (!valueset.hasExpansion() || valueset.getExpansion().getContains().size() == 0) { 829 return null; 830 } else { 831 String cs = valueset.getExpansion().getContains().get(0).getSystem(); 832 if (cs != null && checkSystem(valueset.getExpansion().getContains(), cs)) { 833 return cs; 834 } else { 835 return null; 836 } 837 } 838 } 839 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 840 if (inc.hasValueSet()) { 841 return null; 842 } 843 if (!inc.hasSystem()) { 844 return null; 845 } 846 } 847 if (valueset.getCompose().getInclude().size() == 1) { 848 return valueset.getCompose().getInclude().get(0).getSystem(); 849 } 850 851 return null; 852 } 853 854 /* 855 * Check that all system values within an expansion correspond to the specified system value 856 */ 857 private boolean checkSystem(List<ValueSetExpansionContainsComponent> containsList, String system) { 858 for (ValueSetExpansionContainsComponent contains : containsList) { 859 if (!contains.getSystem().equals(system) || (contains.hasContains() && !checkSystem(contains.getContains(), system))) { 860 return false; 861 } 862 } 863 return true; 864 } 865 866 private ConceptDefinitionComponent findCodeInConcept(ConceptDefinitionComponent concept, String code, AlternateCodesProcessingRules altCodeRules) { 867 opContext.deadCheck(); 868 if (code.equals(concept.getCode())) { 869 return concept; 870 } 871 ConceptDefinitionComponent cc = findCodeInConcept(concept.getConcept(), code, altCodeRules); 872 if (cc != null) { 873 return cc; 874 } 875 if (concept.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) { 876 List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) concept.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK); 877 for (ConceptDefinitionComponent c : children) { 878 cc = findCodeInConcept(c, code, altCodeRules); 879 if (cc != null) { 880 return cc; 881 } 882 } 883 } 884 return null; 885 } 886 887 private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code, AlternateCodesProcessingRules altCodeRules) { 888 for (ConceptDefinitionComponent cc : concept) { 889 if (code.equals(cc.getCode())) { 890 return cc; 891 } 892 if (Utilities.existsInList(code, alternateCodes(cc, altCodeRules))) { 893 return cc; 894 } 895 ConceptDefinitionComponent c = findCodeInConcept(cc, code, altCodeRules); 896 if (c != null) { 897 return c; 898 } 899 } 900 return null; 901 } 902 903 904 private List<String> alternateCodes(ConceptDefinitionComponent focus, AlternateCodesProcessingRules altCodeRules) { 905 List<String> codes = new ArrayList<>(); 906 for (ConceptPropertyComponent p : focus.getProperty()) { 907 if ("alternateCode".equals(p.getCode()) && (altCodeRules.passes(p.getExtension())) && p.getValue().isPrimitive()) { 908 codes.add(p.getValue().primitiveValue()); 909 } 910 } 911 return codes; 912 } 913 914 915 private String systemForCodeInValueSet(String code, List<String> problems) { 916 Set<String> sys = new HashSet<>(); 917 if (!scanForCodeInValueSet(code, sys, problems)) { 918 return null; 919 } 920 if (sys.size() != 1) { 921 problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_MULTIPLE_MATCHES, sys.toString())); 922 return null; 923 } else { 924 return sys.iterator().next(); 925 } 926 } 927 928 private boolean scanForCodeInValueSet(String code, Set<String> sys, List<String> problems) { 929 if (valueset.hasCompose()) { 930 // ignore excludes - they can't make any difference 931 if (!valueset.getCompose().hasInclude() && !valueset.getExpansion().hasContains()) { 932 problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_NO_INCLUDES_OR_EXPANSION, valueset.getVersionedUrl())); 933 } 934 935 int i = 0; 936 for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) { 937 opContext.deadCheck(); 938 if (vsi.hasValueSet()) { 939 for (CanonicalType u : vsi.getValueSet()) { 940 if (!checkForCodeInValueSet(code, u.getValue(), sys, problems)) { 941 return false; 942 } 943 } 944 } else if (!vsi.hasSystem()) { 945 problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_NO_SYSTEM, valueset.getVersionedUrl(), i)); 946 return false; 947 } 948 if (vsi.hasSystem()) { 949 if (vsi.hasFilter()) { 950 problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_NO_SYSTEM, valueset.getVersionedUrl(), i, vsi.getSystem())); 951 return false; 952 } 953 CodeSystemProvider csp = CodeSystemProvider.factory(vsi.getSystem()); 954 if (csp != null) { 955 Boolean ok = csp.checkCode(code); 956 if (ok == null) { 957 problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM_SYSTEM_IS_INDETERMINATE, valueset.getVersionedUrl(), vsi.getSystem())); 958 sys.add(vsi.getSystem()); 959 } else if (ok) { 960 sys.add(vsi.getSystem()); 961 } 962 } else { 963 CodeSystem cs = resolveCodeSystem(vsi.getSystem(), vsi.getVersion()); 964 if (cs != null && cs.getContent() == CodeSystemContentMode.COMPLETE) { 965 966 if (vsi.hasConcept()) { 967 for (ConceptReferenceComponent cc : vsi.getConcept()) { 968 boolean match = cs.getCaseSensitive() ? cc.getCode().equals(code) : cc.getCode().equalsIgnoreCase(code); 969 if (match) { 970 sys.add(vsi.getSystem()); 971 } 972 } 973 } else { 974 ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code, allAltCodes); 975 if (cc != null) { 976 sys.add(vsi.getSystem()); 977 } 978 } 979 } else if (vsi.hasConcept()) { 980 for (ConceptReferenceComponent cc : vsi.getConcept()) { 981 boolean match = cc.getCode().equals(code); 982 if (match) { 983 sys.add(vsi.getSystem()); 984 } 985 } 986 } else { 987 // we'll try to expand this one then 988 ValueSetExpansionOutcome vse = context.expandVS(vsi, false, false); 989 if (vse.isOk()) { 990 if (!checkSystems(vse.getValueset().getExpansion().getContains(), code, sys, problems)) { 991 return false; 992 } 993 } else { 994 problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_UNKNOWN_SYSTEM, valueset.getVersionedUrl(), i, vsi.getSystem(), vse.getAllErrors().toString())); 995 return false; 996 } 997 } 998 } 999 } 1000 i++; 1001 } 1002 } else if (valueset.hasExpansion()) { 1003 // Retrieve a list of all systems associated with this code in the expansion 1004 if (!checkSystems(valueset.getExpansion().getContains(), code, sys, problems)) { 1005 return false; 1006 } 1007 } 1008 return true; 1009 } 1010 1011 private boolean checkForCodeInValueSet(String code, String uri, Set<String> sys, List<String> problems) { 1012 ValueSetValidator vs = getVs(uri); 1013 return vs.scanForCodeInValueSet(code, sys, problems); 1014 } 1015 1016 /* 1017 * Recursively go through all codes in the expansion and for any coding that matches the specified code, add the system for that coding 1018 * to the passed list. 1019 */ 1020 private boolean checkSystems(List<ValueSetExpansionContainsComponent> contains, String code, Set<String> systems, List<String> problems) { 1021 for (ValueSetExpansionContainsComponent c: contains) { 1022 opContext.deadCheck(); 1023 if (c.getCode().equals(code)) { 1024 systems.add(c.getSystem()); 1025 } 1026 if (c.hasContains()) 1027 checkSystems(c.getContains(), code, systems, problems); 1028 } 1029 return true; 1030 } 1031 1032 public Boolean codeInValueSet(String path, String system, String version, String code, ValidationProcessInfo info) throws FHIRException { 1033 if (valueset == null) { 1034 return false; 1035 } 1036 opContext.deadCheck(); 1037 checkCanonical(info.getIssues(), path, valueset, valueset); 1038 Boolean result = false; 1039 VersionInfo vi = new VersionInfo(this); 1040 1041 if (valueset.hasExpansion()) { 1042 return checkExpansion(new Coding(system, code, null), vi); 1043 } else if (valueset.hasCompose()) { 1044 int i = 0; 1045 for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) { 1046 Boolean ok = inComponent(path, vsi, i, system, version, code, valueset.getCompose().getInclude().size() == 1, info); 1047 i++; 1048 if (ok == null && result != null && result == false) { 1049 result = null; 1050 } else if (ok != null && ok) { 1051 result = true; 1052 break; 1053 } 1054 } 1055 i = valueset.getCompose().getInclude().size(); 1056 for (ConceptSetComponent vsi : valueset.getCompose().getExclude()) { 1057 Boolean nok = inComponent(path, vsi, i, system, version, code, valueset.getCompose().getInclude().size() == 1, info); 1058 i++; 1059 if (nok == null && result != null && result == false) { 1060 result = null; 1061 } else if (nok != null && nok) { 1062 result = false; 1063 } 1064 } 1065 } 1066 1067 return result; 1068 } 1069 1070 private Boolean inComponent(String path, ConceptSetComponent vsi, int vsiIndex, String system, String version, String code, boolean only, ValidationProcessInfo info) throws FHIRException { 1071 opContext.deadCheck(); 1072 boolean ok = true; 1073 1074 if (vsi.hasValueSet()) { 1075 if (isValueSetUnionImports()) { 1076 ok = false; 1077 for (UriType uri : vsi.getValueSet()) { 1078 if (inImport(path, uri.getValue(), system, version, code, info)) { 1079 return true; 1080 } 1081 } 1082 } else { 1083 ok = inImport(path, vsi.getValueSet().get(0).getValue(), system, version, code, info); 1084 for (int i = 1; i < vsi.getValueSet().size(); i++) { 1085 UriType uri = vsi.getValueSet().get(i); 1086 ok = ok && inImport(path, uri.getValue(), system, version, code, info); 1087 } 1088 } 1089 } 1090 1091 if (!vsi.hasSystem() || !ok) { 1092 return ok; 1093 } 1094 1095 if (only && system == null) { 1096 // whether we know the system or not, we'll accept the stated codes at face value 1097 for (ConceptReferenceComponent cc : vsi.getConcept()) { 1098 if (cc.getCode().equals(code)) { 1099 return true; 1100 } 1101 } 1102 } 1103 1104 if (system == null || !system.equals(vsi.getSystem())) 1105 return false; 1106 // ok, we need the code system 1107 CodeSystem cs = resolveCodeSystem(system, version); 1108 if (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)) { 1109 if (throwToServer) { 1110 // make up a transient value set with 1111 ValueSet vs = new ValueSet(); 1112 vs.setStatus(PublicationStatus.ACTIVE); 1113 vs.setUrl(valueset.getUrl()+"--"+vsiIndex); 1114 vs.setVersion(valueset.getVersion()); 1115 vs.getCompose().addInclude(vsi); 1116 ValidationResult res = context.validateCode(options.withNoClient(), new Coding(system, code, null), vs); 1117 if (res.getErrorClass() == TerminologyServiceErrorClass.UNKNOWN || res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED || res.getErrorClass() == TerminologyServiceErrorClass.VALUESET_UNSUPPORTED) { 1118 if (info != null && res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) { 1119 // server didn't know the code system either - we'll take it face value 1120 info.addIssue(makeIssue(IssueSeverity.WARNING, IssueType.UNKNOWN, path, context.formatMessage(I18nConstants.TERMINOLOGY_TX_SYSTEM_NOTKNOWN, system))); 1121 for (ConceptReferenceComponent cc : vsi.getConcept()) { 1122 if (cc.getCode().equals(code)) { 1123 return true; 1124 } 1125 } 1126 info.setErr(TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED); 1127 return null; 1128 } 1129 return false; 1130 } 1131 if (res.getErrorClass() == TerminologyServiceErrorClass.NOSERVICE) { 1132 throw new NoTerminologyServiceException(); 1133 } 1134 return res.isOk(); 1135 } else { 1136 if (unknownSystems != null) { 1137 if (version == null) { 1138 unknownSystems.add(system); 1139 } else { 1140 unknownSystems.add(system+"|"+version); 1141 } 1142 } 1143 return null; 1144 } 1145 } else { 1146 checkCanonical(info.getIssues(), path, cs, valueset); 1147 if (valueset.getCompose().hasInactive() && !valueset.getCompose().getInactive()) { 1148 if (CodeSystemUtilities.isInactive(cs, code)) { 1149 return false; 1150 } 1151 } 1152 1153 if (vsi.hasFilter()) { 1154 ok = true; 1155 for (ConceptSetFilterComponent f : vsi.getFilter()) { 1156 if (!codeInFilter(cs, system, f, code)) { 1157 return false; 1158 } 1159 } 1160 } 1161 1162 List<ConceptDefinitionComponent> list = cs.getConcept(); 1163 ok = validateCodeInConceptList(code, cs, list, allAltCodes); 1164 if (ok && vsi.hasConcept()) { 1165 for (ConceptReferenceComponent cc : vsi.getConcept()) { 1166 if (cc.getCode().equals(code)) { 1167 return true; 1168 } 1169 } 1170 return false; 1171 } else { 1172 // recheck that this is a valid alternate code 1173 ok = validateCodeInConceptList(code, cs, list, altCodeParams); 1174 return ok; 1175 } 1176 } 1177 } 1178 1179 protected boolean isValueSetUnionImports() { 1180 PackageInformation p = (PackageInformation) valueset.getSourcePackage(); 1181 if (p != null) { 1182 return p.getDate().before(new GregorianCalendar(2022, Calendar.MARCH, 31).getTime()); 1183 } else { 1184 return false; 1185 } 1186 } 1187 1188 private boolean codeInFilter(CodeSystem cs, String system, ConceptSetFilterComponent f, String code) throws FHIRException { 1189 if ("concept".equals(f.getProperty())) 1190 return codeInConceptFilter(cs, f, code); 1191 else if ("code".equals(f.getProperty()) && f.getOp() == FilterOperator.REGEX) 1192 return codeInRegexFilter(cs, f, code); 1193 else if (CodeSystemUtilities.hasPropertyDef(cs, f.getProperty())) { 1194 return codeInPropertyFilter(cs, f, code); 1195 } else { 1196 System.out.println("todo: handle filters with property = "+f.getProperty()+" "+f.getOp().toCode()); 1197 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__FILTER_WITH_PROPERTY__, cs.getUrl(), f.getProperty(), f.getOp().toCode())); 1198 } 1199 } 1200 1201 private boolean codeInPropertyFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) { 1202 switch (f.getOp()) { 1203 case EQUAL: 1204 if (f.getValue() == null) { 1205 return false; 1206 } 1207 DataType d = CodeSystemUtilities.getProperty(cs, code, f.getProperty()); 1208 return d != null && f.getValue().equals(d.primitiveValue()); 1209 case EXISTS: 1210 return CodeSystemUtilities.getProperty(cs, code, f.getProperty()) != null; 1211 case REGEX: 1212 if (f.getValue() == null) { 1213 return false; 1214 } 1215 d = CodeSystemUtilities.getProperty(cs, code, f.getProperty()); 1216 return d != null && d.primitiveValue() != null && d.primitiveValue().matches(f.getValue()); 1217 default: 1218 System.out.println("todo: handle property filters with op = "+f.getOp()); 1219 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__PROPERTY_FILTER_WITH_OP__, cs.getUrl(), f.getOp())); 1220 } 1221 } 1222 1223 private boolean codeInRegexFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) { 1224 return code.matches(f.getValue()); 1225 } 1226 1227 private boolean codeInConceptFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) throws FHIRException { 1228 switch (f.getOp()) { 1229 case ISA: return codeInConceptIsAFilter(cs, f, code, false); 1230 case ISNOTA: return !codeInConceptIsAFilter(cs, f, code, false); 1231 case DESCENDENTOF: return codeInConceptIsAFilter(cs, f, code, true); 1232 default: 1233 System.out.println("todo: handle concept filters with op = "+f.getOp()); 1234 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__CONCEPT_FILTER_WITH_OP__, cs.getUrl(), f.getOp())); 1235 } 1236 } 1237 1238 private boolean codeInConceptIsAFilter(CodeSystem cs, ConceptSetFilterComponent f, String code, boolean excludeRoot) { 1239 if (!excludeRoot && code.equals(f.getValue())) { 1240 return true; 1241 } 1242 ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), f.getValue(), altCodeParams); 1243 if (cc == null) { 1244 return false; 1245 } 1246 ConceptDefinitionComponent cc2 = findCodeInConcept(cc, code, altCodeParams); 1247 return cc2 != null && cc2 != cc; 1248 } 1249 1250 public boolean validateCodeInConceptList(String code, CodeSystem def, List<ConceptDefinitionComponent> list, AlternateCodesProcessingRules altCodeRules) { 1251 opContext.deadCheck(); 1252 if (def.getCaseSensitive()) { 1253 for (ConceptDefinitionComponent cc : list) { 1254 if (cc.getCode().equals(code)) { 1255 return true; 1256 } 1257 if (Utilities.existsInList(code, alternateCodes(cc, altCodeRules))) { 1258 return true; 1259 } 1260 if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept(), altCodeRules)) { 1261 return true; 1262 } 1263 } 1264 } else { 1265 for (ConceptDefinitionComponent cc : list) { 1266 if (cc.getCode().equalsIgnoreCase(code)) { 1267 return true; 1268 } 1269 if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept(), altCodeRules)) { 1270 return true; 1271 } 1272 } 1273 } 1274 return false; 1275 } 1276 1277 private ValueSetValidator getVs(String url) { 1278 if (inner.containsKey(url)) { 1279 return inner.get(url); 1280 } 1281 ValueSet vs = context.fetchResource(ValueSet.class, url, valueset); 1282 ValueSetValidator vsc = new ValueSetValidator(context, opContext.copy(), options, vs, localContext, expansionProfile, txCaps); 1283 vsc.setThrowToServer(throwToServer); 1284 inner.put(url, vsc); 1285 return vsc; 1286 } 1287 1288 private boolean inImport(String path, String uri, String system, String version, String code, ValidationProcessInfo info) throws FHIRException { 1289 ValueSetValidator vs = getVs(uri); 1290 if (vs == null) { 1291 return false; 1292 } else { 1293 Boolean ok = vs.codeInValueSet(path, system, version, code, info); 1294 return ok != null && ok; 1295 } 1296 } 1297 1298 1299 private String getPreferredDisplay(ConceptReferenceComponent cc) { 1300 if (!options.hasLanguages()) { 1301 return cc.getDisplay(); 1302 } 1303 if (LanguageUtils.langsMatch(options.getLanguages(), valueset.getLanguage())) { 1304 return cc.getDisplay(); 1305 } 1306 // if there's no language, we default to accepting the displays as (US) English 1307 if (valueset.getLanguage() == null && (options.langSummary().contains("en") || options.langSummary().contains("en-US"))) { 1308 return cc.getDisplay(); 1309 } 1310 for (ConceptReferenceDesignationComponent d : cc.getDesignation()) { 1311 if (!d.hasUse() && LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) { 1312 return d.getValue(); 1313 } 1314 } 1315 for (ConceptReferenceDesignationComponent d : cc.getDesignation()) { 1316 if (LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) { 1317 return d.getValue(); 1318 } 1319 } 1320 return cc.getDisplay(); 1321 } 1322 1323 1324 private String getPreferredDisplay(ConceptDefinitionComponent cc, CodeSystem cs) { 1325 if (!options.hasLanguages()) { 1326 return cc.getDisplay(); 1327 } 1328 if (cs != null && LanguageUtils.langsMatch(options.getLanguages(), cs.getLanguage())) { 1329 return cc.getDisplay(); 1330 } 1331 // if there's no language, we default to accepting the displays as (US) English 1332 if ((cs == null || cs.getLanguage() == null) && (options.langSummary().contains("en") || options.langSummary().contains("en-US"))) { 1333 return cc.getDisplay(); 1334 } 1335 for (ConceptDefinitionDesignationComponent d : cc.getDesignation()) { 1336 if (!d.hasUse() && LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) { 1337 return d.getValue(); 1338 } 1339 } 1340 for (ConceptDefinitionDesignationComponent d : cc.getDesignation()) { 1341 if (LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) { 1342 return d.getValue(); 1343 } 1344 } 1345 return cc.getDisplay(); 1346 } 1347 1348}