001package org.hl7.fhir.r5.terminologies.validation; 002 003import java.io.IOException; 004 005/* 006 Copyright (c) 2011+, HL7, Inc. 007 All rights reserved. 008 009 Redistribution and use in source and binary forms, with or without modification, 010 are permitted provided that the following conditions are met: 011 012 * Redistributions of source code must retain the above copyright notice, this 013 list of conditions and the following disclaimer. 014 * Redistributions in binary form must reproduce the above copyright notice, 015 this list of conditions and the following disclaimer in the documentation 016 and/or other materials provided with the distribution. 017 * Neither the name of HL7 nor the names of its contributors may be used to 018 endorse or promote products derived from this software without specific 019 prior written permission. 020 021 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 022 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 023 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 024 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 025 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 026 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 027 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 028 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 029 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 030 POSSIBILITY OF SUCH DAMAGE. 031 032 */ 033 034import java.util.ArrayList; 035import java.util.Arrays; 036import java.util.Calendar; 037import java.util.GregorianCalendar; 038import java.util.HashMap; 039import java.util.HashSet; 040import java.util.List; 041import java.util.Map; 042import java.util.Set; 043 044 045import org.hl7.fhir.exceptions.FHIRException; 046import org.hl7.fhir.exceptions.NoTerminologyServiceException; 047import org.hl7.fhir.r5.context.ContextUtilities; 048import org.hl7.fhir.r5.context.IWorkerContext; 049import org.hl7.fhir.r5.elementmodel.LanguageUtils; 050import org.hl7.fhir.r5.extensions.ExtensionConstants; 051import org.hl7.fhir.r5.model.CanonicalType; 052import org.hl7.fhir.r5.model.CodeSystem; 053import org.hl7.fhir.r5.model.CodeType; 054import org.hl7.fhir.r5.model.Enumerations.CodeSystemContentMode; 055import org.hl7.fhir.r5.model.Enumerations.FilterOperator; 056import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; 057import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionDesignationComponent; 058import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent; 059import org.hl7.fhir.r5.model.CodeableConcept; 060import org.hl7.fhir.r5.model.Coding; 061import org.hl7.fhir.r5.model.DataType; 062import org.hl7.fhir.r5.model.Extension; 063import org.hl7.fhir.r5.model.NamingSystem; 064import org.hl7.fhir.r5.model.Enumerations.PublicationStatus; 065import org.hl7.fhir.r5.model.OperationOutcome.IssueType; 066import org.hl7.fhir.r5.model.OperationOutcome.OperationOutcomeIssueComponent; 067import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent; 068import org.hl7.fhir.r5.model.PackageInformation; 069import org.hl7.fhir.r5.model.Parameters; 070import org.hl7.fhir.r5.model.TerminologyCapabilities.TerminologyCapabilitiesCodeSystemComponent; 071import org.hl7.fhir.r5.model.TerminologyCapabilities; 072import org.hl7.fhir.r5.model.Transport.ParameterComponent; 073import org.hl7.fhir.r5.model.UriType; 074import org.hl7.fhir.r5.model.ValueSet; 075import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent; 076import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceDesignationComponent; 077import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; 078import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent; 079import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; 080import org.hl7.fhir.r5.terminologies.CodeSystemUtilities; 081import org.hl7.fhir.r5.terminologies.client.TerminologyClientManager; 082import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome; 083import org.hl7.fhir.r5.terminologies.providers.CodeSystemProvider; 084import org.hl7.fhir.r5.terminologies.providers.SpecialCodeSystem; 085import org.hl7.fhir.r5.terminologies.providers.URICodeSystem; 086import org.hl7.fhir.r5.terminologies.utilities.TerminologyOperationContext; 087import org.hl7.fhir.r5.terminologies.utilities.TerminologyOperationContext.TerminologyServiceProtectionException; 088import org.hl7.fhir.r5.terminologies.utilities.ValueSetProcessBase.OpIssueCode; 089import org.hl7.fhir.r5.terminologies.validation.ValueSetValidator.StringWithCode; 090import org.hl7.fhir.r5.terminologies.utilities.TerminologyServiceErrorClass; 091import org.hl7.fhir.r5.terminologies.utilities.ValidationResult; 092import org.hl7.fhir.r5.terminologies.utilities.ValueSetProcessBase; 093import org.hl7.fhir.r5.utils.OperationOutcomeUtilities; 094import org.hl7.fhir.r5.utils.ToolingExtensions; 095import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier; 096import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier.ValidationContextResourceProxy; 097import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 098import org.hl7.fhir.utilities.FhirPublication; 099import org.hl7.fhir.utilities.Utilities; 100import org.hl7.fhir.utilities.VersionUtilities; 101import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader; 102import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader.LanguagePreference; 103import org.hl7.fhir.utilities.i18n.I18nConstants; 104import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 105import org.hl7.fhir.utilities.validation.ValidationOptions; 106 107import com.google.j2objc.annotations.ReflectionSupport.Level; 108 109public class ValueSetValidator extends ValueSetProcessBase { 110 111 public static final String NO_TRY_THE_SERVER = "The local terminology server cannot handle this request"; 112 113 114 public class StringWithCode { 115 private OpIssueCode code; 116 private String message; 117 protected StringWithCode(OpIssueCode code, String message) { 118 super(); 119 this.code = code; 120 this.message = message; 121 } 122 public OpIssueCode getCode() { 123 return code; 124 } 125 public String getMessage() { 126 return message; 127 } 128 129 } 130 131 132 private ValueSet valueset; 133 private Map<String, ValueSetValidator> inner = new HashMap<>(); 134 private ValidationOptions options; 135 private ValidationContextCarrier localContext; 136 private List<CodeSystem> localSystems = new ArrayList<>(); 137 protected Parameters expansionProfile; 138 private TerminologyClientManager tcm; 139 private Set<String> unknownSystems; 140 private Set<String> unknownValueSets = new HashSet<>(); 141 private boolean throwToServer; 142 143 public ValueSetValidator(IWorkerContext context, TerminologyOperationContext opContext, ValidationOptions options, ValueSet source, Parameters expansionProfile, TerminologyClientManager tcm) { 144 super(context, opContext); 145 this.valueset = source; 146 this.options = options; 147 this.expansionProfile = expansionProfile; 148 this.tcm = tcm; 149 analyseValueSet(); 150 } 151 152 public ValueSetValidator(IWorkerContext context, TerminologyOperationContext opContext, ValidationOptions options, ValueSet source, ValidationContextCarrier ctxt, Parameters expansionProfile, TerminologyClientManager tcm) { 153 super(context, opContext); 154 this.valueset = source; 155 this.options = options.copy(); 156 this.options.setEnglishOk(true); 157 this.localContext = ctxt; 158 this.expansionProfile = expansionProfile; 159 this.tcm = tcm; 160 analyseValueSet(); 161 } 162 163 public Set<String> getUnknownSystems() { 164 return unknownSystems; 165 } 166 167 public void setUnknownSystems(Set<String> unknownSystems) { 168 this.unknownSystems = unknownSystems; 169 } 170 171 public boolean isThrowToServer() { 172 return throwToServer; 173 } 174 175 public void setThrowToServer(boolean throwToServer) { 176 this.throwToServer = throwToServer; 177 } 178 179 private void analyseValueSet() { 180 if (valueset != null) { 181 opContext.seeContext(valueset.getVersionedUrl()); 182 for (Extension s : valueset.getExtensionsByUrl(ExtensionConstants.EXT_VSSUPPLEMENT)) { 183 requiredSupplements.add(s.getValue().primitiveValue()); 184 } 185 } 186 187 altCodeParams.seeParameters(expansionProfile); 188 altCodeParams.seeValueSet(valueset); 189 if (localContext != null) { 190 if (valueset != null) { 191 for (ConceptSetComponent i : valueset.getCompose().getInclude()) { 192 analyseComponent(i); 193 } 194 for (ConceptSetComponent i : valueset.getCompose().getExclude()) { 195 analyseComponent(i); 196 } 197 } 198 } 199 } 200 201 private void analyseComponent(ConceptSetComponent i) { 202 opContext.deadCheck(); 203 if (i.getSystemElement().hasExtension(ToolingExtensions.EXT_VALUESET_SYSTEM)) { 204 String ref = i.getSystemElement().getExtensionString(ToolingExtensions.EXT_VALUESET_SYSTEM); 205 if (ref.startsWith("#")) { 206 String id = ref.substring(1); 207 for (ValidationContextResourceProxy t : localContext.getResources()) { 208 CodeSystem cs = (CodeSystem) t.loadContainedResource(id, CodeSystem.class); 209 if (cs != null) { 210 localSystems.add(cs); 211 } 212 } 213 } else { 214 throw new Error("Not done yet #2: "+ref); 215 } 216 } 217 } 218 219 public ValidationResult validateCode(CodeableConcept code) throws FHIRException { 220 return validateCode("CodeableConcept", code); 221 } 222 223 public ValidationResult validateCode(String path, CodeableConcept code) throws FHIRException { 224 opContext.deadCheck(); 225 checkValueSetOptions(); 226 227 // first, we validate the codings themselves 228 ValidationProcessInfo info = new ValidationProcessInfo(); 229 230 CodeableConcept vcc = new CodeableConcept(); 231 List<ValidationResult> resList = new ArrayList<>(); 232 233 if (!options.isMembershipOnly()) { 234 int i = 0; 235 for (Coding c : code.getCoding()) { 236 if (!c.hasSystem() && !c.hasUserData("val.sys.error")) { 237 c.setUserData("val.sys.error", true); 238 info.addIssue(makeIssue(IssueSeverity.WARNING, IssueType.INVALID, path+".coding["+i+"]", context.formatMessage(I18nConstants.CODING_HAS_NO_SYSTEM__CANNOT_VALIDATE), OpIssueCode.InvalidData, null)); 239 } else { 240 VersionInfo vi = new VersionInfo(this); 241 checkExpansion(c, vi); 242 checkInclude(c, vi); 243 CodeSystem cs = resolveCodeSystem(c.getSystem(), vi.getVersion(c.getSystem(), c.getVersion())); 244 ValidationResult res = null; 245 if (cs == null || cs.getContent() != CodeSystemContentMode.COMPLETE) { 246 if (context.isNoTerminologyServer()) { 247 if (c.hasVersion()) { 248 String msg = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM_VERSION, c.getSystem(), c.getVersion() , resolveCodeSystemVersions(c.getSystem()).toString()); 249 unknownSystems.add(c.getSystem()+"|"+c.getVersion()); 250 res = new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path+".coding["+i+"].system", msg, OpIssueCode.NotFound, null)).setUnknownSystems(unknownSystems); 251 } else { 252 String msg = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM, c.getSystem(), c.getVersion()); 253 unknownSystems.add(c.getSystem()); 254 res = new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path+".coding["+i+"].system", msg, OpIssueCode.NotFound, null)).setUnknownSystems(unknownSystems); 255 } 256 } else { 257 res = context.validateCode(options.withNoClient(), c, null); 258 if (res.isOk()) { 259 vcc.addCoding(new Coding().setCode(res.getCode()).setVersion(res.getVersion()).setSystem(res.getSystem()).setDisplay(res.getDisplay())); 260 } 261 for (OperationOutcomeIssueComponent iss : res.getIssues()) { 262 if (iss.getSeverity() == org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.ERROR && iss.getDetails().hasCoding("http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", "not-found")) { 263 iss.setSeverity(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.WARNING); 264 res.setSeverity(IssueSeverity.WARNING); 265 } 266 iss.resetPath("Coding", path+".coding["+i+"]"); 267 } 268 if (res.isInactive()) { 269 String msg = context.formatMessage(I18nConstants.STATUS_CODE_WARNING_CODE, "not active", c.getCode()); 270 res.getIssues().addAll(makeIssue(IssueSeverity.INFORMATION, IssueType.INVALID, path+".coding["+i+"].code", msg, OpIssueCode.CodeRule, res.getServer())); 271 } 272 } 273 } else { 274 c.setUserData("cs", cs); 275 276 checkCanonical(info.getIssues(), path, cs, valueset); 277 res = validateCode(path+".coding["+i+"]", c, cs, vcc, info); 278 } 279 info.getIssues().addAll(res.getIssues()); 280 if (res != null) { 281 resList.add(res); 282 if (!res.isOk() && !res.messageIsInIssues()) { 283 if (res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) { 284 info.getIssues().addAll(makeIssue(res.getSeverity(), IssueType.NOTFOUND, path+".coding["+i+"]", res.getMessage(), OpIssueCode.NotFound, res.getServer())); 285 } else { 286 info.getIssues().addAll(makeIssue(res.getSeverity(), IssueType.CODEINVALID, path+".coding["+i+"]", res.getMessage(), OpIssueCode.InvalidCode, res.getServer())); 287 } 288 } 289 } 290 } 291 i++; 292 } 293 } 294 Coding foundCoding = null; 295 String msg = null; 296 Boolean result = false; 297 if (valueset != null) { 298 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(", "); 299 List<String> cpath = new ArrayList<>(); 300 301 int i = 0; 302 for (Coding c : code.getCoding()) { 303 String cs = "'"+c.getSystem()+(c.hasVersion() ? "|"+c.getVersion() : "")+"#"+c.getCode()+(c.hasDisplay() ? " ('"+c.getDisplay()+"')" : "")+"'"; 304 String cs2 = c.getSystem()+(c.hasVersion() ? "|"+c.getVersion() : ""); 305 cpath.add(path+".coding["+i+"]"); 306 b.append(cs2); 307 Boolean ok = codeInValueSet(path, c.getSystem(), c.getVersion(), c.getCode(), info); 308 if (ok == null && result != null && result == false) { 309 result = null; 310 } else if (ok != null && ok) { 311 result = true; 312 foundCoding = c; 313 if (!options.isMembershipOnly()) { 314 vcc.addCoding().setSystem(c.getSystem()).setVersion(c.getVersion()).setCode(c.getCode()); 315 } 316 } 317 if (ok == null || !ok) { 318 vcc.removeCoding(c.getSystem(), c.getVersion(), c.getCode()); 319 } 320 if (ok != null && !ok) { 321 msg = context.formatMessage(I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_ONE, null, valueset.getVersionedUrl(), cs); 322 info.getIssues().addAll(makeIssue(IssueSeverity.INFORMATION, IssueType.CODEINVALID, path+".coding["+i+"].code", msg, OpIssueCode.ThisNotInVS, null)); 323 } 324 i++; 325 } 326 if (result == null) { 327 if (!unknownValueSets.isEmpty()) { 328 msg = context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_VS, valueset.getVersionedUrl(), CommaSeparatedStringBuilder.join(", ", unknownValueSets)); 329 } else { 330 msg = context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_CS, valueset.getVersionedUrl(), b.toString()); 331 } 332 info.getIssues().addAll(makeIssue(IssueSeverity.WARNING, unknownSystems.isEmpty() && unknownValueSets.isEmpty() ? IssueType.CODEINVALID : IssueType.NOTFOUND, null, msg, OpIssueCode.VSProcessing, null)); 333 } else if (!result) { 334 // to match Ontoserver 335 OperationOutcomeIssueComponent iss = new OperationOutcomeIssueComponent(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.ERROR, org.hl7.fhir.r5.model.OperationOutcome.IssueType.CODEINVALID); 336 iss.getDetails().setText(context.formatMessage(I18nConstants.TX_GENERAL_CC_ERROR_MESSAGE, valueset.getVersionedUrl())); 337 iss.getDetails().addCoding("http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", OpIssueCode.NotInVS.toCode(), null); 338 info.getIssues().add(iss); 339 340// msg = context.formatMessagePlural(code.getCoding().size(), I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), b.toString()); 341// info.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, code.getCoding().size() == 1 ? path+".coding[0].code" : path, msg)); 342 } 343 } 344 345 if (vcc.hasCoding() && code.hasText()) { 346 vcc.setText(code.getText()); 347 } 348 if (!checkRequiredSupplements(info)) { 349 return new ValidationResult(IssueSeverity.ERROR, info.getIssues().get(info.getIssues().size()-1).getDetails().getText(), info.getIssues()); 350 } else if (info.hasErrors()) { 351 ValidationResult res = new ValidationResult(IssueSeverity.ERROR, info.summary(), info.getIssues()); 352 if (foundCoding != null) { 353 ConceptDefinitionComponent cd = new ConceptDefinitionComponent(foundCoding.getCode()); 354 cd.setDisplay(lookupDisplay(foundCoding)); 355 res.setDefinition(cd); 356 res.setSystem(foundCoding.getSystem()); 357 res.setVersion(foundCoding.hasVersion() ? foundCoding.getVersion() : foundCoding.hasUserData("cs") ? ((CodeSystem) foundCoding.getUserData("cs")).getVersion() : null); 358 res.setDisplay(cd.getDisplay()); 359 } 360 if (info.getErr() != null) { 361 res.setErrorClass(info.getErr()); 362 } 363 res.setUnknownSystems(unknownSystems); 364 res.addCodeableConcept(vcc); 365 return res; 366 } else if (result == null) { 367 return new ValidationResult(IssueSeverity.WARNING, info.summary(), info.getIssues()); 368 } else if (foundCoding == null && valueset != null) { 369 return new ValidationResult(IssueSeverity.ERROR, "Internal Error that should not happen", makeIssue(IssueSeverity.FATAL, IssueType.EXCEPTION, path, "Internal Error that should not happen", OpIssueCode.VSProcessing, null)); 370 } else if (info.getIssues().size() > 0) { 371 if (foundCoding == null) { 372 IssueSeverity lvl = IssueSeverity.INFORMATION; 373 for (OperationOutcomeIssueComponent iss : info.getIssues()) { 374 lvl = IssueSeverity.max(lvl, OperationOutcomeUtilities.convert(iss.getSeverity())); 375 } 376 return new ValidationResult(lvl, info.summary(), info.getIssues()); 377 } else { 378 String disp = lookupDisplay(foundCoding); 379 ConceptDefinitionComponent cd = new ConceptDefinitionComponent(foundCoding.getCode()); 380 cd.setDisplay(disp); 381 return new ValidationResult(IssueSeverity.WARNING, info.summaryList(), foundCoding.getSystem(), getVersion(foundCoding), cd, disp, info.getIssues()).addCodeableConcept(vcc); 382 } 383 } else if (!result) { 384 if (valueset != null) { 385 throw new Error("what?"); 386 } else if (vcc.hasCoding()) { 387 return new ValidationResult(vcc.getCodingFirstRep().getSystem(), getVersion(vcc.getCodingFirstRep()), new ConceptDefinitionComponent(vcc.getCodingFirstRep().getCode()).setDisplay(vcc.getCodingFirstRep().getDisplay()), vcc.getCodingFirstRep().getDisplay()).addCodeableConcept(vcc); 388 } else { 389 throw new Error("what?"); 390 } 391 } else if (foundCoding != null) { 392 ConceptDefinitionComponent cd = new ConceptDefinitionComponent(foundCoding.getCode()); 393 cd.setDisplay(lookupDisplay(foundCoding)); 394 return new ValidationResult(foundCoding.getSystem(), getVersion(foundCoding), cd, getPreferredDisplay(cd, null)).addCodeableConcept(vcc); 395 } else { 396 throw new Error("what?"); 397 } 398 } 399 400 private boolean checkRequiredSupplements(ValidationProcessInfo info) { 401 if (!requiredSupplements.isEmpty()) { 402 String msg= context.formatMessagePlural(requiredSupplements.size(), I18nConstants.VALUESET_SUPPLEMENT_MISSING, CommaSeparatedStringBuilder.build(requiredSupplements)); 403 throw new TerminologyServiceProtectionException(msg, TerminologyServiceErrorClass.BUSINESS_RULE, IssueType.NOTFOUND); 404 } 405 return requiredSupplements.isEmpty(); 406 } 407 408 private boolean valueSetDependsOn(String system, String version) { 409 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 410 if (system.equals(inc.getSystem()) && (version == null || inc.getVersion() == null || version.equals(inc.getVersion()))) { 411 return true; 412 } 413 } 414 return false; 415 } 416 417 private String getVersion(Coding c) { 418 if (c.hasVersion()) { 419 return c.getVersion(); 420 } else if (c.hasUserData("cs")) { 421 return ((CodeSystem) c.getUserData("cs")).getVersion(); 422 } else { 423 return null; 424 } 425 } 426 427 private String lookupDisplay(Coding c) { 428 CodeSystem cs = resolveCodeSystem(c.getSystem(), c.getVersion()); 429 if (cs != null) { 430 ConceptDefinitionComponent cd = CodeSystemUtilities.findCodeOrAltCode(cs.getConcept(), c.getCode(), null); 431 if (cd != null) { 432 return getPreferredDisplay(cd, cs); 433 } 434 } 435 return null; 436 } 437 438 public CodeSystem resolveCodeSystem(String system, String version) { 439 for (CodeSystem t : localSystems) { 440 if (t.getUrl().equals(system) && versionsMatch(version, t.getVersion())) { 441 return t; 442 } 443 } 444 CodeSystem cs = context.fetchSupplementedCodeSystem(system, version); 445 if (cs == null) { 446 cs = findSpecialCodeSystem(system, version); 447 } 448 return cs; 449 } 450 451 public List<String> resolveCodeSystemVersions(String system) { 452 List<String> res = new ArrayList<>(); 453 for (CodeSystem t : localSystems) { 454 if (t.getUrl().equals(system) && t.hasVersion()) { 455 res.add(t.getVersion()); 456 } 457 } 458 res.addAll(new ContextUtilities(context).fetchCodeSystemVersions(system)); 459 return res; 460 } 461 462 private boolean versionsMatch(String versionTest, String versionActual) { 463 return versionTest == null && VersionUtilities.versionsMatch(versionTest, versionActual); 464 } 465 466 public ValidationResult validateCode(Coding code) throws FHIRException { 467 return validateCode("Coding", code); 468 } 469 470 public ValidationResult validateCode(String path, Coding code) throws FHIRException { 471 opContext.deadCheck(); 472 checkValueSetOptions(); 473 474 String warningMessage = null; 475 // first, we validate the concept itself 476 477 ValidationResult res = null; 478 boolean inExpansion = false; 479 boolean inInclude = false; 480 List<OperationOutcomeIssueComponent> issues = new ArrayList<>(); 481 ValidationProcessInfo info = new ValidationProcessInfo(issues); 482 VersionInfo vi = new VersionInfo(this); 483 checkCanonical(issues, path, valueset, valueset); 484 485 String system = code.getSystem(); 486 if (!options.isMembershipOnly()) { 487 if (system == null && !code.hasDisplay() && options.isGuessSystem()) { // dealing with just a plain code (enum) 488 List<StringWithCode> problems = new ArrayList<>(); 489 system = systemForCodeInValueSet(code.getCode(), problems); 490 if (system == null) { 491 if (problems.size() == 0) { 492 throw new Error("Unable to resolve systems but no reason why"); // this is an error in the java code 493 } else if (problems.size() == 1) { 494 String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), "'"+code.toString()+"'"); 495 issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, "code", msg, OpIssueCode.NotInVS, null)); 496 issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, "code", problems.get(0).getMessage(), problems.get(0).getCode(), null)); 497 return new ValidationResult(IssueSeverity.ERROR, problems.get(0).getMessage()+"; "+msg, issues); 498 } else { 499 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder("; "); 500 for (StringWithCode s : problems) { 501 b.append(s.getMessage()); 502 } 503 ValidationResult vr = new ValidationResult(IssueSeverity.ERROR, b.toString(), null); 504 for (StringWithCode s : problems) { 505 vr.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.UNKNOWN, path, s.getMessage(), s.getCode(), vr.getServer())); 506 } 507 return vr; 508 } 509 } 510 } 511 if (!code.hasSystem()) { 512 if (options.isGuessSystem() && system == null && Utilities.isAbsoluteUrl(code.getCode())) { 513 system = "urn:ietf:rfc:3986"; // this arises when using URIs bound to value sets 514 } 515 code.setSystem(system); 516 } 517 if (!code.hasSystem()) { 518 res = new ValidationResult(IssueSeverity.WARNING, context.formatMessage(I18nConstants.CODING_HAS_NO_SYSTEM__CANNOT_VALIDATE), null); 519 if (!code.hasUserData("val.sys.error")) { 520 code.setUserData("val.sys.error", true); 521 res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.INVALID, path, context.formatMessage(I18nConstants.CODING_HAS_NO_SYSTEM__CANNOT_VALIDATE), OpIssueCode.InvalidData, null)); 522 } 523 } else { 524 if (!Utilities.isAbsoluteUrl(system)) { 525 String msg = context.formatMessage(I18nConstants.TERMINOLOGY_TX_SYSTEM_RELATIVE); 526 issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path+".system", msg, OpIssueCode.InvalidData, null)); 527 } 528 inExpansion = checkExpansion(code, vi); 529 inInclude = checkInclude(code, vi); 530 String wv = vi.getVersion(system, code.getVersion()); 531 CodeSystem cs = resolveCodeSystem(system, wv); 532 if (cs == null) { 533 OpIssueCode oic = OpIssueCode.NotFound; 534 IssueType itype = IssueType.NOTFOUND; 535 ValueSet vs = context.fetchResource(ValueSet.class, system); 536 if (vs != null) { 537 warningMessage = context.formatMessage(I18nConstants.TERMINOLOGY_TX_SYSTEM_VALUESET2, system); 538 oic = OpIssueCode.InvalidData; 539 itype = IssueType.INVALID; 540 } else if (wv == null) { 541 warningMessage = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM, system); 542 unknownSystems.add(system); 543 } else { 544 warningMessage = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM_VERSION, system, wv, resolveCodeSystemVersions(system).toString()); 545 unknownSystems.add(system+"|"+wv); 546 } 547 if (!inExpansion) { 548 if (valueset != null && valueset.hasExpansion()) { 549 String msg = context.formatMessage(I18nConstants.CODESYSTEM_CS_UNK_EXPANSION, 550 valueset.getUrl(), 551 code.getSystem(), 552 code.getCode().toString()); 553 issues.addAll(makeIssue(IssueSeverity.ERROR, itype, path, msg, OpIssueCode.VSProcessing, null)); 554 throw new VSCheckerException(msg, issues, TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED); 555 } else { 556 issues.addAll(makeIssue(IssueSeverity.ERROR, itype, path+".system", warningMessage, oic, null)); 557 res = new ValidationResult(IssueSeverity.WARNING, warningMessage, issues); 558 if (valueset == null) { 559 throw new VSCheckerException(warningMessage, issues, TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED); 560 } else { 561 // String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl(), code.toString()); 562 // issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path, msg)); 563 // we don't do this yet 564 // throw new VSCheckerException(warningMessage, issues); 565 } 566 } 567 } 568 } else { 569 checkCanonical(issues, path, cs, valueset); 570 } 571 if (cs != null && cs.hasSupplements()) { 572 String msg = context.formatMessage(I18nConstants.CODESYSTEM_CS_NO_SUPPLEMENT, cs.getUrl()); 573 return new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path+".system", msg, OpIssueCode.InvalidData, null)); 574 } 575 if (cs!=null && cs.getContent() != CodeSystemContentMode.COMPLETE) { 576 warningMessage = "Resolved system "+system+(cs.hasVersion() ? " (v"+cs.getVersion()+")" : "")+", but the definition "; 577 switch (cs.getContent()) { 578 case EXAMPLE: 579 warningMessage = warningMessage +"only has example content"; 580 break; 581 case FRAGMENT: 582 warningMessage = warningMessage + "is only a fragment"; 583 break; 584 case NOTPRESENT: 585 warningMessage = warningMessage + "doesn't include any codes"; 586 break; 587 case SUPPLEMENT: 588 warningMessage = warningMessage + " is for a supplement to "+cs.getSupplements(); 589 break; 590 default: 591 break; 592 } 593 warningMessage = warningMessage + ", so the code has not been validated"; 594 if (!options.isExampleOK() && !inExpansion && cs.getContent() != CodeSystemContentMode.FRAGMENT) { // we're going to give it a go if it's a fragment 595 throw new VSCheckerException(warningMessage, null, true); 596 } 597 } 598 599 if (cs != null /*&& (cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT)*/) { 600 if (!(cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT || 601 (options.isExampleOK() && cs.getContent() == CodeSystemContentMode.EXAMPLE))) { 602 if (inInclude) { 603 ConceptReferenceComponent cc = findInInclude(code); 604 if (cc != null) { 605 // we'll take it on faith 606 String disp = getPreferredDisplay(cc); 607 res = new ValidationResult(system, cs.getVersion(), new ConceptDefinitionComponent().setCode(cc.getCode()).setDisplay(disp), disp); 608 res.addMessage("Resolved system "+system+", but the definition is not complete, so assuming value set include is correct"); 609 return res; 610 } 611 } 612 // we can't validate that here. 613 throw new FHIRException("Unable to evaluate based on code system with status = "+cs.getContent().toCode()); 614 } 615 res = validateCode(path, code, cs, null, info); 616 res.setIssues(issues); 617 } else if (cs == null && valueset.hasExpansion() && inExpansion) { 618 // we just take the value set as face value then 619 res = new ValidationResult(system, wv, new ConceptDefinitionComponent().setCode(code.getCode()).setDisplay(code.getDisplay()), code.getDisplay()); 620 if (!preferServerSide(system)) { 621 res.addMessage("Code System unknown, so assuming value set expansion is correct ("+warningMessage+")"); 622 } 623 } else { 624 // well, we didn't find a code system - try the expansion? 625 // disabled waiting for discussion 626 if (throwToServer) { 627 throw new FHIRException(NO_TRY_THE_SERVER); 628 } 629 } 630 } 631 } else { 632 inExpansion = checkExpansion(code, vi); 633 inInclude = checkInclude(code, vi); 634 } 635 String wv = vi.getVersion(system, code.getVersion()); 636 if (!checkRequiredSupplements(info)) { 637 return new ValidationResult(IssueSeverity.ERROR, issues.get(issues.size()-1).getDetails().getText(), issues); 638 } 639 640 641 // then, if we have a value set, we check it's in the value set 642 if (valueset != null) { 643 if ((res==null || res.isOk())) { 644 Boolean ok = codeInValueSet(path, system, wv, code.getCode(), info); 645 if (ok == null || !ok) { 646 if (res == null) { 647 res = new ValidationResult((IssueSeverity) null, null, info.getIssues()); 648 } 649 if (info.getErr() != null) { 650 res.setErrorClass(info.getErr()); 651 } 652 if (ok == null) { 653 String m = null; 654 if (!unknownSystems.isEmpty()) { 655 m = context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_CS, valueset.getVersionedUrl(), CommaSeparatedStringBuilder.join(",", unknownSystems)); 656 } else if (!unknownValueSets.isEmpty()) { 657 res.addMessage(info.getIssues().get(0).getDetails().getText()); 658 m = context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_VS, valueset.getVersionedUrl(), CommaSeparatedStringBuilder.join(",", unknownValueSets)); 659 } else { 660 // not sure why we'd get to here? 661 m = context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl()); 662 } 663 res.addMessage(m); 664 res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.NOTFOUND, null, m, OpIssueCode.VSProcessing, null)); 665 res.setUnknownSystems(unknownSystems); 666 res.setSeverity(IssueSeverity.ERROR); // back patching for display logic issue 667 res.setErrorClass(TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED); 668 } else if (!inExpansion && !inInclude) { 669// if (!info.getIssues().isEmpty()) { 670// res.setMessage("Not in value set "+valueset.getUrl()+": "+info.summary()).setSeverity(IssueSeverity.ERROR); 671// res.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path, res.getMessage())); 672// } else 673// { 674 String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), "'"+code.toString()+"'"); 675 res.addMessage(msg).setSeverity(IssueSeverity.ERROR); 676 res.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path+".code", msg, OpIssueCode.NotInVS, null)); 677 res.setDefinition(null); 678 res.setSystem(null); 679 res.setDisplay(null); 680 res.setUnknownSystems(unknownSystems); 681// } 682 } else if (warningMessage!=null) { 683 String msg = context.formatMessage(I18nConstants.CODE_FOUND_IN_EXPANSION_HOWEVER_, warningMessage); 684 res = new ValidationResult(IssueSeverity.WARNING, msg, makeIssue(IssueSeverity.WARNING, IssueType.EXCEPTION, path, msg, OpIssueCode.VSProcessing, null)); 685 } else if (inExpansion) { 686 res.setMessage("Code found in expansion, however: " + res.getMessage()); 687 res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.EXCEPTION, path, res.getMessage(), OpIssueCode.VSProcessing, null)); 688 } else if (inInclude) { 689 res.setMessage("Code found in include, however: " + res.getMessage()); 690 res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.EXCEPTION, path, res.getMessage(), OpIssueCode.VSProcessing, null)); 691 } 692 } else if (res == null) { 693 res = new ValidationResult(system, wv, null, null); 694 } 695 } else if ((res != null && !res.isOk())) { 696 String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), "'"+code.toString()+"'"); 697 res.addMessage(msg); 698 res.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path+".code", msg, OpIssueCode.NotInVS, null)); 699 } 700 } 701 if (res != null && res.getSeverity() == IssueSeverity.INFORMATION && res.getMessage() != null) { 702 res.setSeverity(IssueSeverity.ERROR); // back patching for display logic issue 703 } 704 return res; 705 } 706 707 private void checkValueSetOptions() { 708 if (valueset != null) { 709 for (Extension ext : valueset.getCompose().getExtensionsByUrl("http://hl7.org/fhir/tools/StructureDefinion/valueset-expansion-param")) { 710 var name = ext.getExtensionString("name"); 711 var value = ext.getExtensionByUrl("value").getValue(); 712 if ("displayLanguage".equals(name)) { 713 options.setLanguages(value.primitiveValue()); 714 } 715 } 716 if (!options.hasLanguages() && valueset.hasLanguage()) { 717 options.addLanguage(valueset.getLanguage()); 718 } 719 } 720 } 721 722 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", 723 "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/", 724 "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")); 725 726 private boolean preferServerSide(String system) { 727 if (SERVER_SIDE_LIST.contains(system)) { 728 return true; 729 } 730 731 try { 732 if (tcm.supportsSystem(system)) { 733 return true; 734 } 735 } catch (IOException e) { 736 e.printStackTrace(); 737 } 738 739 return false; 740 } 741 742 private boolean checkInclude(Coding code, VersionInfo vi) { 743 if (valueset == null || code.getSystem() == null || code.getCode() == null) { 744 return false; 745 } 746 for (ConceptSetComponent inc : valueset.getCompose().getExclude()) { 747 if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) { 748 for (ConceptReferenceComponent cc : inc.getConcept()) { 749 if (cc.hasCode() && cc.getCode().equals(code.getCode())) { 750 return false; 751 } 752 } 753 } 754 } 755 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 756 if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) { 757 vi.setComposeVersion(inc.getVersion()); 758 for (ConceptReferenceComponent cc : inc.getConcept()) { 759 if (cc.hasCode() && cc.getCode().equals(code.getCode())) { 760 return true; 761 } 762 } 763 } 764 } 765 return false; 766 } 767 768 private ConceptReferenceComponent findInInclude(Coding code) { 769 if (valueset == null || code.getSystem() == null || code.getCode() == null) { 770 return null; 771 } 772 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 773 if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) { 774 for (ConceptReferenceComponent cc : inc.getConcept()) { 775 if (cc.hasCode() && cc.getCode().equals(code.getCode())) { 776 return cc; 777 } 778 } 779 } 780 } 781 return null; 782 } 783 784 private CodeSystem findSpecialCodeSystem(String system, String version) { 785 if ("urn:ietf:rfc:3986".equals(system)) { 786 CodeSystem cs = new CodeSystem(); 787 cs.setUrl(system); 788 cs.setUserData("tx.cs.special", new URICodeSystem()); 789 cs.setContent(CodeSystemContentMode.COMPLETE); 790 return cs; 791 } 792 return null; 793 } 794 795 private ValidationResult findCodeInExpansion(Coding code) { 796 if (valueset==null || !valueset.hasExpansion()) 797 return null; 798 return findCodeInExpansion(code, valueset.getExpansion().getContains()); 799 } 800 801 private ValidationResult findCodeInExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) { 802 for (ValueSetExpansionContainsComponent containsComponent: contains) { 803 opContext.deadCheck(); 804 if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) { 805 ConceptDefinitionComponent ccd = new ConceptDefinitionComponent(); 806 ccd.setCode(containsComponent.getCode()); 807 ccd.setDisplay(containsComponent.getDisplay()); 808 ValidationResult res = new ValidationResult(code.getSystem(), code.hasVersion() ? code.getVersion() : containsComponent.getVersion(), ccd, getPreferredDisplay(ccd, null)); 809 return res; 810 } 811 if (containsComponent.hasContains()) { 812 ValidationResult res = findCodeInExpansion(code, containsComponent.getContains()); 813 if (res != null) { 814 return res; 815 } 816 } 817 } 818 return null; 819 } 820 821 private boolean checkExpansion(Coding code, VersionInfo vi) { 822 if (valueset==null || !valueset.hasExpansion()) { 823 return false; 824 } 825 return checkExpansion(code, valueset.getExpansion().getContains(), vi); 826 } 827 828 private boolean checkExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains, VersionInfo vi) { 829 for (ValueSetExpansionContainsComponent containsComponent: contains) { 830 opContext.deadCheck(); 831 if (containsComponent.hasSystem() && containsComponent.hasCode() && containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) { 832 vi.setExpansionVersion(containsComponent.getVersion()); 833 return true; 834 } 835 if (containsComponent.hasContains() && checkExpansion(code, containsComponent.getContains(), vi)) { 836 return true; 837 } 838 } 839 return false; 840 } 841 842 private ValidationResult validateCode(String path, Coding code, CodeSystem cs, CodeableConcept vcc, ValidationProcessInfo info) { 843 ConceptDefinitionComponent cc = cs.hasUserData("tx.cs.special") ? ((SpecialCodeSystem) cs.getUserData("tx.cs.special")).findConcept(code) : findCodeInConcept(cs.getConcept(), code.getCode(), cs.getCaseSensitive(), allAltCodes); 844 if (cc == null) { 845 cc = findSpecialConcept(code, cs); 846 } 847 if (cc == null) { 848 if (cs.getContent() == CodeSystemContentMode.FRAGMENT) { 849 String msg = context.formatMessage(I18nConstants.UNKNOWN_CODE_IN_FRAGMENT, code.getCode(), cs.getUrl(), cs.getVersion()); 850 return new ValidationResult(IssueSeverity.WARNING, msg, makeIssue(IssueSeverity.WARNING, IssueType.CODEINVALID, path+".code", msg, OpIssueCode.InvalidCode, null)); 851 } else { 852 String msg = context.formatMessage(I18nConstants.UNKNOWN_CODE_IN_VERSION, code.getCode(), cs.getUrl(), cs.getVersion()); 853 return new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path+".code", msg, OpIssueCode.InvalidCode, null)); 854 } 855 } else { 856 if (!cc.getCode().equals(code.getCode())) { 857 String msg = context.formatMessage(I18nConstants.CODE_CASE_DIFFERENCE, code.getCode(), cc.getCode(), cs.getVersionedUrl()); 858 info.addIssue(makeIssue(IssueSeverity.INFORMATION, IssueType.BUSINESSRULE, path+".code", msg, OpIssueCode.CodeRule, null)); 859 } 860 } 861 Coding vc = new Coding().setCode(cc.getCode()).setSystem(cs.getUrl()).setVersion(cs.getVersion()).setDisplay(getPreferredDisplay(cc, cs)); 862 if (vcc != null) { 863 vcc.addCoding(vc); 864 } 865 866 boolean inactive = (CodeSystemUtilities.isInactive(cs, cc)); 867 String status = inactive ? (CodeSystemUtilities.getStatus(cs, cc)) : null; 868 869 boolean isDefaultLang = false; 870 boolean ws = false; 871 if (code.getDisplay() == null) { 872 return new ValidationResult(code.getSystem(), cs.getVersion(), cc, vc.getDisplay()).setStatus(inactive, status); 873 } 874 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(", ", " or "); 875 if (cc.hasDisplay() && isOkLanguage(cs.getLanguage())) { 876 b.append("'"+cc.getDisplay()+"'"+(cs.hasLanguage() ? " ("+cs.getLanguage()+")" : "")); 877 if (code.getDisplay().equalsIgnoreCase(cc.getDisplay())) { 878 return new ValidationResult(code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status); 879 } else if (Utilities.normalize(code.getDisplay()).equals(Utilities.normalize(cc.getDisplay()))) { 880 ws = true; 881 } 882 } else if (cc.hasDisplay() && code.getDisplay().equalsIgnoreCase(cc.getDisplay())) { 883 isDefaultLang = true; 884 } 885 886 for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) { 887 opContext.deadCheck(); 888 if (isOkLanguage(ds.getLanguage())) { 889 b.append("'"+ds.getValue()+"' ("+ds.getLanguage()+")"); 890 if (code.getDisplay().equalsIgnoreCase(ds.getValue())) { 891 return new ValidationResult(code.getSystem(),cs.getVersion(), cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status); 892 } 893 if (Utilities.normalize(code.getDisplay()).equalsIgnoreCase(Utilities.normalize(ds.getValue()))) { 894 ws = true; 895 } 896 } 897 } 898 // also check to see if the value set has another display 899 if (options.isUseValueSetDisplays()) { 900 ConceptReferencePair vs = findValueSetRef(code.getSystem(), code.getCode()); 901 if (vs != null && (vs.getCc().hasDisplay() ||vs.getCc().hasDesignation())) { 902 if (vs.getCc().hasDisplay() && isOkLanguage(vs.getValueset().getLanguage())) { 903 b.append("'"+vs.getCc().getDisplay()+"'"); 904 if (code.getDisplay().equalsIgnoreCase(vs.getCc().getDisplay())) { 905 return new ValidationResult(code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status); 906 } 907 } 908 for (ConceptReferenceDesignationComponent ds : vs.getCc().getDesignation()) { 909 opContext.deadCheck(); 910 if (isOkLanguage(ds.getLanguage())) { 911 b.append("'"+ds.getValue()+"'"); 912 if (code.getDisplay().equalsIgnoreCase(ds.getValue())) { 913 return new ValidationResult(code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status); 914 } 915 } 916 } 917 } 918 } 919 if (b.count() > 0) { 920 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()); 921 return new ValidationResult(dispWarningStatus(), msg, code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs), makeIssue(dispWarning(), IssueType.INVALID, path+".display", msg, OpIssueCode.Display, null)).setStatus(inactive, status); 922 } else if (isDefaultLang) { 923 // we didn't find any valid displays because there aren't any, so the default language is acceptable, but we'll still add a hint about that 924 boolean none = options.getLanguages().getLangs().size() == 1 && !hasLanguage(cs, options.getLanguages().getLangs().get(0)); 925 String msg = context.formatMessagePlural(options.getLanguages().getLangs().size(), none ? I18nConstants.NO_VALID_DISPLAY_FOUND_LANG_NONE : I18nConstants.NO_VALID_DISPLAY_FOUND_LANG_SOME, code.getSystem(), code.getCode(), code.getDisplay(), options.langSummary(), code.getDisplay()); 926 String n = null; 927 return new ValidationResult(IssueSeverity.INFORMATION, n, code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs), makeIssue(IssueSeverity.INFORMATION, IssueType.INVALID, path+".display", msg, OpIssueCode.DisplayComment, null)).setStatus(inactive, status); 928 } else { 929 String msg = context.formatMessagePlural(options.getLanguages().getLangs().size(), I18nConstants.NO_VALID_DISPLAY_FOUND, code.getSystem(), code.getCode(), code.getDisplay(), options.langSummary()); 930 return new ValidationResult(IssueSeverity.WARNING, msg, code.getSystem(), cs.getVersion(), cc, cc.getDisplay(), makeIssue(IssueSeverity.WARNING, IssueType.INVALID, path+".display", msg, OpIssueCode.Display, null)).setStatus(inactive, status); 931 } 932 } 933 934 private boolean hasLanguage(CodeSystem cs, LanguagePreference languagePreference) { 935 String lang = languagePreference.getLang(); 936 if (lang == null) { 937 return false; 938 } 939 for (ConceptDefinitionComponent cc : cs.getConcept()) { 940 boolean hl = hasLanguage(cs, cc, lang); 941 if (hl) { 942 return true; 943 } 944 } 945 return false; 946 } 947 948 private boolean hasLanguage(CodeSystem cs, ConceptDefinitionComponent cc, String lang) { 949 if (lang.equals(cs.getLanguage()) && cc.hasDisplay()) { 950 return true; 951 } 952 for (ConceptDefinitionDesignationComponent d : cc.getDesignation()) { 953 if (lang.equals(d.getLanguage())) { 954 return true; 955 } 956 } 957 for (ConceptDefinitionComponent cc1 : cc.getConcept()) { 958 boolean hl = hasLanguage(cs, cc1, lang); 959 if (hl) { 960 return true; 961 } 962 } 963 return false; 964 } 965 966 private ConceptDefinitionComponent findSpecialConcept(Coding c, CodeSystem cs) { 967 // handling weird special cases in v2 code systems 968 if ("http://terminology.hl7.org/CodeSystem/v2-0203".equals(cs.getUrl())) { 969 String code = c.getCode(); 970 if (code != null && code.startsWith("NN") && code.length() > 3) { 971 ConceptDefinitionComponent cd = findCountryCode(code.substring(2)); 972 if (cd != null) { 973 return new ConceptDefinitionComponent(code).setDisplay("National Identifier for "+cd.getDisplay()); 974 } 975 } 976 } 977// 0396: HL7nnnn, IBTnnnn, ISOnnnn, X12Dennnn, 99zzz 978// 0335: PRNxxx 979 return null; 980 } 981 982 983 private ConceptDefinitionComponent findCountryCode(String code) { 984 ValidationResult vr = context.validateCode(new ValidationOptions(FhirPublication.R5), "urn:iso:std:iso:3166", null, code, null); 985 return vr == null || !vr.isOk() ? null : new ConceptDefinitionComponent(code).setDisplay(vr.getDisplay()).setDefinition(vr.getDefinition()); 986 } 987 988 private IssueSeverity dispWarning() { 989 return options.isDisplayWarningMode() ? IssueSeverity.WARNING : IssueSeverity.ERROR; 990 } 991 992 private IssueSeverity dispWarningStatus() { 993 return options.isDisplayWarningMode() ? IssueSeverity.WARNING : IssueSeverity.INFORMATION; // information -> error later 994 } 995 996 private boolean isOkLanguage(String language) { 997 if (!options.hasLanguages()) { 998 return true; 999 } 1000 if (LanguageUtils.langsMatch(options.getLanguages(), language)) { 1001 return true; 1002 } 1003 if (language == null && (options.langSummary().contains("en") || options.langSummary().contains("en-US") || options.isEnglishOk())) { 1004 return true; 1005 } 1006 return false; 1007 } 1008 1009 private ConceptReferencePair findValueSetRef(String system, String code) { 1010 if (valueset == null) 1011 return null; 1012 // if it has an expansion 1013 for (ValueSetExpansionContainsComponent exp : valueset.getExpansion().getContains()) { 1014 opContext.deadCheck(); 1015 if (system.equals(exp.getSystem()) && code.equals(exp.getCode())) { 1016 ConceptReferenceComponent cc = new ConceptReferenceComponent(); 1017 cc.setDisplay(exp.getDisplay()); 1018 cc.setDesignation(exp.getDesignation()); 1019 return new ConceptReferencePair(valueset, cc); 1020 } 1021 } 1022 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 1023 if (system.equals(inc.getSystem())) { 1024 for (ConceptReferenceComponent cc : inc.getConcept()) { 1025 if (cc.getCode().equals(code)) { 1026 return new ConceptReferencePair(valueset, cc); 1027 } 1028 } 1029 } 1030 for (CanonicalType url : inc.getValueSet()) { 1031 ConceptReferencePair cc = getVs(url.asStringValue(), null).findValueSetRef(system, code); 1032 if (cc != null) { 1033 return cc; 1034 } 1035 } 1036 } 1037 return null; 1038 } 1039 1040 /* 1041 * Check that all system values within an expansion correspond to the specified system value 1042 */ 1043 private boolean checkSystem(List<ValueSetExpansionContainsComponent> containsList, String system) { 1044 for (ValueSetExpansionContainsComponent contains : containsList) { 1045 if (!contains.getSystem().equals(system) || (contains.hasContains() && !checkSystem(contains.getContains(), system))) { 1046 return false; 1047 } 1048 } 1049 return true; 1050 } 1051 1052 private ConceptDefinitionComponent findCodeInConcept(ConceptDefinitionComponent concept, String code, boolean caseSensitive, AlternateCodesProcessingRules altCodeRules) { 1053 opContext.deadCheck(); 1054 if (code.equals(concept.getCode())) { 1055 return concept; 1056 } 1057 ConceptDefinitionComponent cc = findCodeInConcept(concept.getConcept(), code, caseSensitive, altCodeRules); 1058 if (cc != null) { 1059 return cc; 1060 } 1061 if (concept.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) { 1062 List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) concept.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK); 1063 for (ConceptDefinitionComponent c : children) { 1064 cc = findCodeInConcept(c, code, caseSensitive, altCodeRules); 1065 if (cc != null) { 1066 return cc; 1067 } 1068 } 1069 } 1070 return null; 1071 } 1072 1073 private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code, boolean caseSensitive, AlternateCodesProcessingRules altCodeRules) { 1074 for (ConceptDefinitionComponent cc : concept) { 1075 if (code.equals(cc.getCode()) || (!caseSensitive && (code.equalsIgnoreCase(cc.getCode())))) { 1076 return cc; 1077 } 1078 if (Utilities.existsInList(code, alternateCodes(cc, altCodeRules))) { 1079 return cc; 1080 } 1081 ConceptDefinitionComponent c = findCodeInConcept(cc, code, caseSensitive, altCodeRules); 1082 if (c != null) { 1083 return c; 1084 } 1085 } 1086 return null; 1087 } 1088 1089 1090 private List<String> alternateCodes(ConceptDefinitionComponent focus, AlternateCodesProcessingRules altCodeRules) { 1091 List<String> codes = new ArrayList<>(); 1092 for (ConceptPropertyComponent p : focus.getProperty()) { 1093 if ("alternateCode".equals(p.getCode()) && (altCodeRules.passes(p.getExtension())) && p.getValue().isPrimitive()) { 1094 codes.add(p.getValue().primitiveValue()); 1095 } 1096 } 1097 return codes; 1098 } 1099 1100 1101 private String systemForCodeInValueSet(String code, List<StringWithCode> problems) { 1102 Set<String> sys = new HashSet<>(); 1103 if (!scanForCodeInValueSet(code, sys, problems)) { 1104 return null; 1105 } 1106 if (sys.size() == 0) { 1107 problems.add(new StringWithCode(OpIssueCode.InferFailed, context.formatMessage(I18nConstants.UNABLE_TO_INFER_CODESYSTEM, code, valueset.getVersionedUrl()))); 1108 return null; 1109 } else if (sys.size() > 1) { 1110 problems.add(new StringWithCode(OpIssueCode.InferFailed, context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_MULTIPLE_MATCHES, code, valueset.getVersionedUrl(), sys.toString()))); 1111 return null; 1112 } else { 1113 return sys.iterator().next(); 1114 } 1115 } 1116 1117 private boolean scanForCodeInValueSet(String code, Set<String> sys, List<StringWithCode> problems) { 1118 if (valueset.hasCompose()) { 1119 // ignore excludes - they can't make any difference 1120 if (!valueset.getCompose().hasInclude() && !valueset.getExpansion().hasContains()) { 1121 problems.add(new StringWithCode(OpIssueCode.InferFailed, context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_NO_INCLUDES_OR_EXPANSION, code, valueset.getVersionedUrl()))); 1122 } 1123 1124 int i = 0; 1125 for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) { 1126 opContext.deadCheck(); 1127 if (scanForCodeInValueSetInclude(code, sys, problems, i, vsi)) { 1128 return true; 1129 } 1130 i++; 1131 } 1132 } else if (valueset.hasExpansion()) { 1133 // Retrieve a list of all systems associated with this code in the expansion 1134 if (!checkSystems(valueset.getExpansion().getContains(), code, sys, problems)) { 1135 return false; 1136 } 1137 } 1138 return true; 1139 } 1140 1141 private boolean scanForCodeInValueSetInclude(String code, Set<String> sys, List<StringWithCode> problems, int i, ConceptSetComponent vsi) { 1142 if (vsi.hasValueSet()) { 1143 for (CanonicalType u : vsi.getValueSet()) { 1144 if (!checkForCodeInValueSet(code, u.getValue(), sys, problems)) { 1145 return false; 1146 } 1147 } 1148 } else if (!vsi.hasSystem()) { 1149 problems.add(new StringWithCode(OpIssueCode.InferFailed, context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_NO_SYSTEM, code, valueset.getVersionedUrl(), i))); 1150 return false; 1151 } 1152 if (vsi.hasSystem()) { 1153 if (vsi.hasFilter()) { 1154 ValueSet vsDummy = new ValueSet(); 1155 vsDummy.setUrl(Utilities.makeUuidUrn()); 1156 vsDummy.setStatus(PublicationStatus.ACTIVE); 1157 vsDummy.getCompose().addInclude(vsi); 1158 Coding c = new Coding().setCode(code).setSystem(vsi.getSystem()); 1159 ValidationResult vr = context.validateCode(options.withGuessSystem(false), c, vsDummy); 1160 if (vr.isOk()) { 1161 sys.add(vsi.getSystem()); 1162 } else { 1163 // problems.add(new StringWithCode(OpIssueCode.InferFailed, context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_FILTER, code, valueset.getVersionedUrl(), i, vsi.getSystem(), filterSummary(vsi)))); 1164 return false; 1165 } 1166 } 1167 CodeSystemProvider csp = CodeSystemProvider.factory(vsi.getSystem()); 1168 if (csp != null) { 1169 Boolean ok = csp.checkCode(code); 1170 if (ok == null) { 1171 problems.add(new StringWithCode(OpIssueCode.InferFailed, context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM_SYSTEM_IS_INDETERMINATE, code, valueset.getVersionedUrl(), vsi.getSystem()))); 1172 sys.add(vsi.getSystem()); 1173 } else if (ok) { 1174 sys.add(vsi.getSystem()); 1175 } 1176 } else { 1177 CodeSystem cs = resolveCodeSystem(vsi.getSystem(), vsi.getVersion()); 1178 if (cs != null && cs.getContent() == CodeSystemContentMode.COMPLETE) { 1179 1180 if (vsi.hasConcept()) { 1181 for (ConceptReferenceComponent cc : vsi.getConcept()) { 1182 boolean match = cs.getCaseSensitive() ? cc.getCode().equals(code) : cc.getCode().equalsIgnoreCase(code); 1183 if (match) { 1184 sys.add(vsi.getSystem()); 1185 } 1186 } 1187 } else { 1188 ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code, cs.getCaseSensitive(), allAltCodes); 1189 if (cc != null) { 1190 sys.add(vsi.getSystem()); 1191 } 1192 } 1193 } else if (vsi.hasConcept()) { 1194 for (ConceptReferenceComponent cc : vsi.getConcept()) { 1195 boolean match = cc.getCode().equals(code); 1196 if (match) { 1197 sys.add(vsi.getSystem()); 1198 } 1199 } 1200 } else { 1201 ValueSet vsDummy = new ValueSet(); 1202 vsDummy.setUrl(Utilities.makeUuidUrn()); 1203 vsDummy.setStatus(PublicationStatus.ACTIVE); 1204 vsDummy.getCompose().addInclude(vsi); 1205 ValidationResult vr = context.validateCode(options.withNoClient(), code, vsDummy); 1206 if (vr.isOk()) { 1207 sys.add(vsi.getSystem()); 1208 } else { 1209 // ok, we'll try to expand this one then 1210 ValueSetExpansionOutcome vse = context.expandVS(vsi, false, false); 1211 if (vse.isOk()) { 1212 if (!checkSystems(vse.getValueset().getExpansion().getContains(), code, sys, problems)) { 1213 return false; 1214 } 1215 } else { 1216 problems.add(new StringWithCode(OpIssueCode.NotFound, context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_UNKNOWN_SYSTEM, code, valueset.getVersionedUrl(), i, vsi.getSystem(), vse.getAllErrors().toString()))); 1217 return false; 1218 } 1219 1220 } 1221 } 1222 } 1223 } 1224 return false; 1225 } 1226 1227 private String filterSummary(ConceptSetComponent vsi) { 1228 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 1229 for (ConceptSetFilterComponent f : vsi.getFilter()) { 1230 b.append(f.getProperty()+f.getOp().toCode()+f.getValue()); 1231 } 1232 return b.toString(); 1233 } 1234 1235 private boolean checkForCodeInValueSet(String code, String uri, Set<String> sys, List<StringWithCode> problems) { 1236 ValueSetValidator vs = getVs(uri, null); 1237 return vs.scanForCodeInValueSet(code, sys, problems); 1238 } 1239 1240 /* 1241 * Recursively go through all codes in the expansion and for any coding that matches the specified code, add the system for that coding 1242 * to the passed list. 1243 */ 1244 private boolean checkSystems(List<ValueSetExpansionContainsComponent> contains, String code, Set<String> systems, List<StringWithCode> problems) { 1245 for (ValueSetExpansionContainsComponent c: contains) { 1246 opContext.deadCheck(); 1247 if (c.getCode().equals(code)) { 1248 systems.add(c.getSystem()); 1249 } 1250 if (c.hasContains()) 1251 checkSystems(c.getContains(), code, systems, problems); 1252 } 1253 return true; 1254 } 1255 1256 public Boolean codeInValueSet(String path, String system, String version, String code, ValidationProcessInfo info) throws FHIRException { 1257 if (valueset == null) { 1258 return null; 1259 } 1260 opContext.deadCheck(); 1261 checkCanonical(info.getIssues(), path, valueset, valueset); 1262 Boolean result = false; 1263 VersionInfo vi = new VersionInfo(this); 1264 1265 if (valueset.hasExpansion()) { 1266 return checkExpansion(new Coding(system, code, null), vi); 1267 } else if (valueset.hasCompose()) { 1268 int i = 0; 1269 for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) { 1270 Boolean ok = inComponent(path, vsi, i, system, version, code, valueset.getCompose().getInclude().size() == 1, info); 1271 i++; 1272 if (ok == null && result != null && result == false) { 1273 result = null; 1274 } else if (ok != null && ok) { 1275 result = true; 1276 break; 1277 } 1278 } 1279 i = valueset.getCompose().getInclude().size(); 1280 for (ConceptSetComponent vsi : valueset.getCompose().getExclude()) { 1281 Boolean nok = inComponent(path, vsi, i, system, version, code, valueset.getCompose().getInclude().size() == 1, info); 1282 i++; 1283 if (nok == null && result != null && result == false) { 1284 result = null; 1285 } else if (nok != null && nok) { 1286 result = false; 1287 } 1288 } 1289 } 1290 1291 return result; 1292 } 1293 1294 private Boolean inComponent(String path, ConceptSetComponent vsi, int vsiIndex, String system, String version, String code, boolean only, ValidationProcessInfo info) throws FHIRException { 1295 opContext.deadCheck(); 1296 boolean ok = true; 1297 1298 if (vsi.hasValueSet()) { 1299 if (isValueSetUnionImports()) { 1300 ok = false; 1301 for (UriType uri : vsi.getValueSet()) { 1302 if (inImport(path, uri.getValue(), system, version, code, info)) { 1303 return true; 1304 } 1305 } 1306 } else { 1307 Boolean bok = inImport(path, vsi.getValueSet().get(0).getValue(), system, version, code, info); 1308 if (bok == null) { 1309 return bok; 1310 } 1311 ok = bok; 1312 for (int i = 1; i < vsi.getValueSet().size(); i++) { 1313 UriType uri = vsi.getValueSet().get(i); 1314 ok = ok && inImport(path, uri.getValue(), system, version, code, info); 1315 } 1316 } 1317 } 1318 1319 if (!vsi.hasSystem() || !ok) { 1320 return ok; 1321 } 1322 1323 if (only && system == null) { 1324 // whether we know the system or not, we'll accept the stated codes at face value 1325 for (ConceptReferenceComponent cc : vsi.getConcept()) { 1326 if (cc.getCode().equals(code)) { 1327 return true; 1328 } 1329 } 1330 } 1331 1332 if (system == null || !system.equals(vsi.getSystem())) 1333 return false; 1334 // ok, we need the code system 1335 CodeSystem cs = resolveCodeSystem(system, version); 1336 if (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)) { 1337 if (throwToServer) { 1338 // make up a transient value set with 1339 ValueSet vs = new ValueSet(); 1340 vs.setStatus(PublicationStatus.ACTIVE); 1341 vs.setUrl(valueset.getUrl()+"--"+vsiIndex); 1342 vs.setVersion(valueset.getVersion()); 1343 vs.getCompose().addInclude(vsi); 1344 ValidationResult res = context.validateCode(options.withNoClient(), new Coding(system, code, null), vs); 1345 if (res.getErrorClass() == TerminologyServiceErrorClass.UNKNOWN || res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED || res.getErrorClass() == TerminologyServiceErrorClass.VALUESET_UNSUPPORTED) { 1346 if (info != null && res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) { 1347 // server didn't know the code system either - we'll take it face value 1348 if (!info.hasNotFound(system)) { 1349 String msg = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM, system); 1350 info.addIssue(makeIssue(IssueSeverity.WARNING, IssueType.UNKNOWN, path, msg, OpIssueCode.NotFound, null)); 1351 for (ConceptReferenceComponent cc : vsi.getConcept()) { 1352 if (cc.getCode().equals(code)) { 1353 return true; 1354 } 1355 } 1356 } 1357 info.setErr(TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED); 1358 return null; 1359 } 1360 return false; 1361 } 1362 if (res.getErrorClass() == TerminologyServiceErrorClass.NOSERVICE) { 1363 throw new NoTerminologyServiceException(); 1364 } 1365 return res.isOk(); 1366 } else { 1367 info.setErr(TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED); 1368 if (unknownSystems != null) { 1369 if (version == null) { 1370 unknownSystems.add(system); 1371 } else { 1372 unknownSystems.add(system+"|"+version); 1373 } 1374 } 1375 return null; 1376 } 1377 } else { 1378 checkCanonical(info.getIssues(), path, cs, valueset); 1379 if ((valueset.getCompose().hasInactive() && !valueset.getCompose().getInactive()) || options.isActiveOnly()) { 1380 if (CodeSystemUtilities.isInactive(cs, code)) { 1381 info.addIssue(makeIssue(IssueSeverity.ERROR, IssueType.BUSINESSRULE, path+".code", context.formatMessage(I18nConstants.STATUS_CODE_WARNING_CODE, "not active", code), OpIssueCode.CodeRule, null)); 1382 return false; 1383 } 1384 } 1385 1386 if (vsi.hasFilter()) { 1387 ok = true; 1388 for (ConceptSetFilterComponent f : vsi.getFilter()) { 1389 if (!codeInFilter(cs, system, f, code)) { 1390 return false; 1391 } 1392 } 1393 } 1394 1395 List<ConceptDefinitionComponent> list = cs.getConcept(); 1396 ok = validateCodeInConceptList(code, cs, list, allAltCodes); 1397 if (ok && vsi.hasConcept()) { 1398 for (ConceptReferenceComponent cc : vsi.getConcept()) { 1399 if (cc.getCode().equals(code)) { 1400 return true; 1401 } 1402 } 1403 return false; 1404 } else { 1405 // recheck that this is a valid alternate code 1406 ok = validateCodeInConceptList(code, cs, list, altCodeParams); 1407 return ok; 1408 } 1409 } 1410 } 1411 1412 protected boolean isValueSetUnionImports() { 1413 PackageInformation p = (PackageInformation) valueset.getSourcePackage(); 1414 if (p != null) { 1415 return p.getDate().before(new GregorianCalendar(2022, Calendar.MARCH, 31).getTime()); 1416 } else { 1417 return false; 1418 } 1419 } 1420 1421 private boolean codeInFilter(CodeSystem cs, String system, ConceptSetFilterComponent f, String code) throws FHIRException { 1422 if ("concept".equals(f.getProperty())) 1423 return codeInConceptFilter(cs, f, code); 1424 else if ("code".equals(f.getProperty()) && f.getOp() == FilterOperator.REGEX) 1425 return codeInRegexFilter(cs, f, code); 1426 else if (CodeSystemUtilities.isDefinedProperty(cs, f.getProperty())) { 1427 return codeInPropertyFilter(cs, f, code); 1428 } else if (isKnownProperty(f.getProperty())) { 1429 return codeInKnownPropertyFilter(cs, f, code); 1430 } else { 1431 System.out.println("todo: handle filters with property = "+f.getProperty()+" "+f.getOp().toCode()); 1432 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__FILTER_WITH_PROPERTY__, cs.getUrl(), f.getProperty(), f.getOp().toCode())); 1433 } 1434 } 1435 1436 private boolean isKnownProperty(String code) { 1437 return Utilities.existsInList(code, "notSelectable"); 1438 } 1439 1440 private boolean codeInPropertyFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) { 1441 switch (f.getOp()) { 1442 case EQUAL: 1443 if (f.getValue() == null) { 1444 return false; 1445 } 1446 DataType d = CodeSystemUtilities.getProperty(cs, code, f.getProperty()); 1447 return d != null && f.getValue().equals(d.primitiveValue()); 1448 case EXISTS: 1449 return CodeSystemUtilities.getProperty(cs, code, f.getProperty()) != null; 1450 case REGEX: 1451 if (f.getValue() == null) { 1452 return false; 1453 } 1454 d = CodeSystemUtilities.getProperty(cs, code, f.getProperty()); 1455 return d != null && d.primitiveValue() != null && d.primitiveValue().matches(f.getValue()); 1456 case IN: 1457 if (f.getValue() == null) { 1458 return false; 1459 } 1460 String[] values = f.getValue().split("\\,"); 1461 d = CodeSystemUtilities.getProperty(cs, code, f.getProperty()); 1462 if (d != null) { 1463 String v = d.primitiveValue(); 1464 for (String value : values) { 1465 if (v != null && v.equals(value.trim())) { 1466 return true; 1467 } 1468 } 1469 } 1470 return false; 1471 case NOTIN: 1472 if (f.getValue() == null) { 1473 return true; 1474 } 1475 values = f.getValue().split("\\,"); 1476 d = CodeSystemUtilities.getProperty(cs, code, f.getProperty()); 1477 if (d != null) { 1478 String v = d.primitiveValue(); 1479 for (String value : values) { 1480 if (v != null && v.equals(value.trim())) { 1481 return false; 1482 } 1483 } 1484 } 1485 return true; 1486 default: 1487 System.out.println("todo: handle property filters with op = "+f.getOp()); 1488 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__PROPERTY_FILTER_WITH_OP__, cs.getUrl(), f.getOp())); 1489 } 1490 } 1491 1492 private boolean codeInKnownPropertyFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) { 1493 1494 switch (f.getOp()) { 1495 case EQUAL: 1496 if (f.getValue() == null) { 1497 return false; 1498 } 1499 DataType d = CodeSystemUtilities.getProperty(cs, code, f.getProperty()); 1500 return d != null && f.getValue().equals(d.primitiveValue()); 1501 case EXISTS: 1502 return CodeSystemUtilities.getProperty(cs, code, f.getProperty()) != null; 1503 case REGEX: 1504 if (f.getValue() == null) { 1505 return false; 1506 } 1507 d = CodeSystemUtilities.getProperty(cs, code, f.getProperty()); 1508 return d != null && d.primitiveValue() != null && d.primitiveValue().matches(f.getValue()); 1509 default: 1510 System.out.println("todo: handle known property filters with op = "+f.getOp()); 1511 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__PROPERTY_FILTER_WITH_OP__, cs.getUrl(), f.getOp())); 1512 } 1513 } 1514 1515 private boolean codeInRegexFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) { 1516 return code.matches(f.getValue()); 1517 } 1518 1519 private boolean codeInConceptFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) throws FHIRException { 1520 switch (f.getOp()) { 1521 case ISA: return codeInConceptIsAFilter(cs, f, code, false); 1522 case ISNOTA: return !codeInConceptIsAFilter(cs, f, code, false); 1523 case DESCENDENTOF: return codeInConceptIsAFilter(cs, f, code, true); 1524 default: 1525 System.out.println("todo: handle concept filters with op = "+f.getOp()); 1526 throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__CONCEPT_FILTER_WITH_OP__, cs.getUrl(), f.getOp())); 1527 } 1528 } 1529 1530 private boolean codeInConceptIsAFilter(CodeSystem cs, ConceptSetFilterComponent f, String code, boolean excludeRoot) { 1531 if (!excludeRoot && code.equals(f.getValue())) { 1532 return true; 1533 } 1534 ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), f.getValue(), cs.getCaseSensitive(), altCodeParams); 1535 if (cc == null) { 1536 return false; 1537 } 1538 ConceptDefinitionComponent cc2 = findCodeInConcept(cc, code, cs.getCaseSensitive(), altCodeParams); 1539 return cc2 != null && cc2 != cc; 1540 } 1541 1542 public boolean validateCodeInConceptList(String code, CodeSystem def, List<ConceptDefinitionComponent> list, AlternateCodesProcessingRules altCodeRules) { 1543 opContext.deadCheck(); 1544 if (def.hasUserData("tx.cs.special")) { 1545 return ((SpecialCodeSystem) def.getUserData("tx.cs.special")).findConcept(new Coding().setCode(code)) != null; 1546 } else if (def.getCaseSensitive()) { 1547 for (ConceptDefinitionComponent cc : list) { 1548 if (cc.getCode().equals(code)) { 1549 return true; 1550 } 1551 if (Utilities.existsInList(code, alternateCodes(cc, altCodeRules))) { 1552 return true; 1553 } 1554 if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept(), altCodeRules)) { 1555 return true; 1556 } 1557 } 1558 } else { 1559 for (ConceptDefinitionComponent cc : list) { 1560 if (cc.getCode().equalsIgnoreCase(code)) { 1561 return true; 1562 } 1563 if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept(), altCodeRules)) { 1564 return true; 1565 } 1566 } 1567 } 1568 return false; 1569 } 1570 1571 private ValueSetValidator getVs(String url, ValidationProcessInfo info) { 1572 if (inner.containsKey(url)) { 1573 return inner.get(url); 1574 } 1575 ValueSet vs = context.findTxResource(ValueSet.class, url, valueset); 1576 if (vs == null && info != null) { 1577 unknownValueSets.add(url); 1578 info.addIssue(makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, null, context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_VALUE_SET_, url), OpIssueCode.NotFound, null)); 1579 } 1580 ValueSetValidator vsc = new ValueSetValidator(context, opContext.copy(), options, vs, localContext, expansionProfile, tcm); 1581 vsc.setThrowToServer(throwToServer); 1582 inner.put(url, vsc); 1583 return vsc; 1584 } 1585 1586 private Boolean inImport(String path, String uri, String system, String version, String code, ValidationProcessInfo info) throws FHIRException { 1587 ValueSetValidator vs = getVs(uri, info); 1588 if (vs == null) { 1589 return false; 1590 } else { 1591 Boolean ok = vs.codeInValueSet(path, system, version, code, info); 1592 return ok; 1593 } 1594 } 1595 1596 1597 private String getPreferredDisplay(ConceptReferenceComponent cc) { 1598 if (!options.hasLanguages()) { 1599 return cc.getDisplay(); 1600 } 1601 if (LanguageUtils.langsMatch(options.getLanguages(), valueset.getLanguage())) { 1602 return cc.getDisplay(); 1603 } 1604 // if there's no language, we default to accepting the displays as (US) English 1605 if (valueset.getLanguage() == null && (options.langSummary().contains("en") || options.langSummary().contains("en-US"))) { 1606 return cc.getDisplay(); 1607 } 1608 for (ConceptReferenceDesignationComponent d : cc.getDesignation()) { 1609 if (!d.hasUse() && LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) { 1610 return d.getValue(); 1611 } 1612 } 1613 for (ConceptReferenceDesignationComponent d : cc.getDesignation()) { 1614 if (LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) { 1615 return d.getValue(); 1616 } 1617 } 1618 return cc.getDisplay(); 1619 } 1620 1621 1622 private String getPreferredDisplay(ConceptDefinitionComponent cc, CodeSystem cs) { 1623 if (!options.hasLanguages()) { 1624 return cc.getDisplay(); 1625 } 1626 if (cs != null && LanguageUtils.langsMatch(options.getLanguages(), cs.getLanguage())) { 1627 return cc.getDisplay(); 1628 } 1629 // if there's no language, we default to accepting the displays as (US) English 1630 if ((cs == null || cs.getLanguage() == null) && (options.langSummary().contains("en") || options.langSummary().contains("en-US"))) { 1631 return cc.getDisplay(); 1632 } 1633 for (ConceptDefinitionDesignationComponent d : cc.getDesignation()) { 1634 if (!d.hasUse() && LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) { 1635 return d.getValue(); 1636 } 1637 } 1638 for (ConceptDefinitionDesignationComponent d : cc.getDesignation()) { 1639 if (LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) { 1640 return d.getValue(); 1641 } 1642 } 1643 return cc.getDisplay(); 1644 } 1645 1646}