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