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