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