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