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