
001package org.hl7.fhir.r4.terminologies; 002 003import java.util.ArrayList; 004import java.util.HashMap; 005import java.util.List; 006import java.util.Map; 007 008/* 009 Copyright (c) 2011+, HL7, Inc. 010 All rights reserved. 011 012 Redistribution and use in source and binary forms, with or without modification, 013 are permitted provided that the following conditions are met: 014 015 * Redistributions of source code must retain the above copyright notice, this 016 list of conditions and the following disclaimer. 017 * Redistributions in binary form must reproduce the above copyright notice, 018 this list of conditions and the following disclaimer in the documentation 019 and/or other materials provided with the distribution. 020 * Neither the name of HL7 nor the names of its contributors may be used to 021 endorse or promote products derived from this software without specific 022 prior written permission. 023 024 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 025 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 026 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 027 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 028 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 029 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 030 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 031 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 032 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 033 POSSIBILITY OF SUCH DAMAGE. 034 035 */ 036 037import lombok.extern.slf4j.Slf4j; 038import org.hl7.fhir.exceptions.FHIRException; 039import org.hl7.fhir.r4.context.IWorkerContext; 040import org.hl7.fhir.r4.context.IWorkerContext.ValidationResult; 041import org.hl7.fhir.r4.model.CanonicalType; 042import org.hl7.fhir.r4.model.CodeSystem; 043import org.hl7.fhir.r4.model.CodeSystem.CodeSystemContentMode; 044import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent; 045import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionDesignationComponent; 046import org.hl7.fhir.r4.model.CodeableConcept; 047import org.hl7.fhir.r4.model.Coding; 048import org.hl7.fhir.r4.model.UriType; 049import org.hl7.fhir.r4.model.ValueSet; 050import org.hl7.fhir.r4.model.ValueSet.ConceptReferenceComponent; 051import org.hl7.fhir.r4.model.ValueSet.ConceptReferenceDesignationComponent; 052import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent; 053import org.hl7.fhir.r4.model.ValueSet.ConceptSetFilterComponent; 054import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent; 055import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 056import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage; 057import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 058import org.hl7.fhir.utilities.validation.ValidationOptions; 059 060@MarkedToMoveToAdjunctPackage 061@Slf4j 062public class ValueSetCheckerSimple implements ValueSetChecker { 063 064 private ValueSet valueset; 065 private IWorkerContext context; 066 private Map<String, ValueSetCheckerSimple> inner = new HashMap<>(); 067 private ValidationOptions options; 068 069 public ValueSetCheckerSimple(ValidationOptions options, ValueSet source, IWorkerContext context) { 070 this.valueset = source; 071 this.context = context; 072 this.options = options; 073 } 074 075 public ValidationResult validateCode(CodeableConcept code) throws FHIRException { 076 // first, we validate the codings themselves 077 List<String> errors = new ArrayList<String>(); 078 List<String> warnings = new ArrayList<String>(); 079 for (Coding c : code.getCoding()) { 080 if (!c.hasSystem()) 081 warnings.add("Coding has no system"); 082 CodeSystem cs = context.fetchCodeSystem(c.getSystem()); 083 if (cs == null) 084 warnings.add("Unsupported system " + c.getSystem() + " - system is not specified or implicit"); 085 else if (cs.getContent() != CodeSystemContentMode.COMPLETE) 086 warnings.add("Unable to resolve system " + c.getSystem() + " - system is not complete"); 087 else { 088 ValidationResult res = validateCode(c, cs); 089 if (!res.isOk()) 090 errors.add(res.getMessage()); 091 else if (res.getMessage() != null) 092 warnings.add(res.getMessage()); 093 } 094 } 095 if (valueset != null) { 096 boolean ok = false; 097 for (Coding c : code.getCoding()) { 098 ok = ok || codeInValueSet(c.getSystem(), c.getCode()); 099 } 100 if (!ok) 101 errors.add(0, "None of the provided codes are in the value set " + valueset.getUrl()); 102 } 103 if (errors.size() > 0) 104 return new ValidationResult(IssueSeverity.ERROR, errors.toString()); 105 else if (warnings.size() > 0) 106 return new ValidationResult(IssueSeverity.WARNING, warnings.toString()); 107 else 108 return new ValidationResult(IssueSeverity.INFORMATION, null); 109 } 110 111 public ValidationResult validateCode(Coding code) throws FHIRException { 112 String warningMessage = null; 113 // first, we validate the concept itself 114 115 String system = code.hasSystem() ? code.getSystem() : getValueSetSystem(); 116 if (system == null && !code.hasDisplay()) { // dealing with just a plain code (enum) 117 system = systemForCodeInValueSet(code.getCode()); 118 } 119 if (!code.hasSystem()) 120 code.setSystem(system); 121 boolean inExpansion = checkExpansion(code); 122 CodeSystem cs = context.fetchCodeSystem(system); 123 if (cs == null) { 124 warningMessage = "Unable to resolve system " + system + " - system is not specified or implicit"; 125 if (!inExpansion) 126 throw new FHIRException(warningMessage); 127 } 128 if (cs != null && cs.getContent() != CodeSystemContentMode.COMPLETE) { 129 warningMessage = "Unable to resolve system " + system + " - system is not complete"; 130 if (!inExpansion) 131 throw new FHIRException(warningMessage); 132 } 133 134 ValidationResult res = null; 135 if (cs != null) 136 res = validateCode(code, cs); 137 138 // then, if we have a value set, we check it's in the value set 139 if ((res == null || res.isOk()) && valueset != null && !codeInValueSet(system, code.getCode())) { 140 if (!inExpansion) 141 res.setMessage("Not in value set " + valueset.getUrl()).setSeverity(IssueSeverity.ERROR); 142 else if (warningMessage != null) 143 res = new ValidationResult(IssueSeverity.WARNING, "Code found in expansion, however: " + warningMessage); 144 else 145 res.setMessage("Code found in expansion, however: " + res.getMessage()); 146 } 147 return res; 148 } 149 150 boolean checkExpansion(Coding code) { 151 if (valueset == null || !valueset.hasExpansion()) 152 return false; 153 return checkExpansion(code, valueset.getExpansion().getContains()); 154 } 155 156 boolean checkExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) { 157 for (ValueSetExpansionContainsComponent containsComponent : contains) { 158 if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) 159 return true; 160 if (containsComponent.hasContains() && checkExpansion(code, containsComponent.getContains())) 161 return true; 162 } 163 return false; 164 } 165 166 private ValidationResult validateCode(Coding code, CodeSystem cs) { 167 ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code.getCode()); 168 if (cc == null) 169 return new ValidationResult(IssueSeverity.ERROR, "Unknown Code " + gen(code) + " in " + cs.getUrl()); 170 if (code.getDisplay() == null) 171 return new ValidationResult(cc); 172 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 173 if (cc.hasDisplay()) { 174 b.append(cc.getDisplay()); 175 if (code.getDisplay().equalsIgnoreCase(cc.getDisplay())) 176 return new ValidationResult(cc); 177 } 178 for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) { 179 b.append(ds.getValue()); 180 if (code.getDisplay().equalsIgnoreCase(ds.getValue())) 181 return new ValidationResult(cc); 182 } 183 // also check to see if the value set has another display 184 ConceptReferenceComponent vs = findValueSetRef(code.getSystem(), code.getCode()); 185 if (vs != null && (vs.hasDisplay() || vs.hasDesignation())) { 186 if (vs.hasDisplay()) { 187 b.append(vs.getDisplay()); 188 if (code.getDisplay().equalsIgnoreCase(vs.getDisplay())) 189 return new ValidationResult(cc); 190 } 191 for (ConceptReferenceDesignationComponent ds : vs.getDesignation()) { 192 b.append(ds.getValue()); 193 if (code.getDisplay().equalsIgnoreCase(ds.getValue())) 194 return new ValidationResult(cc); 195 } 196 } 197 return new ValidationResult(IssueSeverity.WARNING, "Display Name for " + code.getSystem() + "#" + code.getCode() 198 + " should be one of '" + b.toString() + "' instead of " + code.getDisplay(), cc); 199 } 200 201 private ConceptReferenceComponent findValueSetRef(String system, String code) { 202 if (valueset == null) 203 return null; 204 // if it has an expansion 205 for (ValueSetExpansionContainsComponent exp : valueset.getExpansion().getContains()) { 206 if (system.equals(exp.getSystem()) && code.equals(exp.getCode())) { 207 ConceptReferenceComponent cc = new ConceptReferenceComponent(); 208 cc.setDisplay(exp.getDisplay()); 209 cc.setDesignation(exp.getDesignation()); 210 return cc; 211 } 212 } 213 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 214 if (system.equals(inc.getSystem())) { 215 for (ConceptReferenceComponent cc : inc.getConcept()) { 216 if (cc.getCode().equals(code)) 217 return cc; 218 } 219 } 220 for (CanonicalType url : inc.getValueSet()) { 221 ConceptReferenceComponent cc = getVs(url.asStringValue()).findValueSetRef(system, code); 222 if (cc != null) 223 return cc; 224 } 225 } 226 return null; 227 } 228 229 private String gen(Coding code) { 230 if (code.hasSystem()) 231 return code.getSystem() + "#" + code.getCode(); 232 else 233 return null; 234 } 235 236 private String getValueSetSystem() throws FHIRException { 237 if (valueset == null) 238 throw new FHIRException("Unable to resolve system - no value set"); 239 if (valueset.getCompose().getInclude().size() == 0) { 240 if (!valueset.hasExpansion() || valueset.getExpansion().getContains().size() == 0) 241 throw new FHIRException("Unable to resolve system - value set has no includes or expansion"); 242 else { 243 String cs = valueset.getExpansion().getContains().get(0).getSystem(); 244 if (cs != null && checkSystem(valueset.getExpansion().getContains(), cs)) 245 return cs; 246 else 247 throw new FHIRException("Unable to resolve system - value set expansion has multiple systems"); 248 } 249 } 250 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 251 if (inc.hasValueSet()) 252 throw new FHIRException("Unable to resolve system - value set has imports"); 253 if (!inc.hasSystem()) 254 throw new FHIRException("Unable to resolve system - value set has include with no system"); 255 } 256 if (valueset.getCompose().getInclude().size() == 1) 257 return valueset.getCompose().getInclude().get(0).getSystem(); 258 259 return null; 260 } 261 262 /* 263 * Check that all system values within an expansion correspond to the specified 264 * system value 265 */ 266 private boolean checkSystem(List<ValueSetExpansionContainsComponent> containsList, String system) { 267 for (ValueSetExpansionContainsComponent contains : containsList) { 268 if (!contains.getSystem().equals(system) 269 || (contains.hasContains() && !checkSystem(contains.getContains(), system))) 270 return false; 271 } 272 return true; 273 } 274 275 private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code) { 276 for (ConceptDefinitionComponent cc : concept) { 277 if (code.equals(cc.getCode())) 278 return cc; 279 ConceptDefinitionComponent c = findCodeInConcept(cc.getConcept(), code); 280 if (c != null) 281 return c; 282 } 283 return null; 284 } 285 286 private String systemForCodeInValueSet(String code) { 287 String sys = null; 288 if (valueset.hasCompose()) { 289 if (valueset.getCompose().hasExclude()) 290 return null; 291 for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) { 292 if (vsi.hasValueSet()) 293 return null; 294 if (!vsi.hasSystem()) 295 return null; 296 if (vsi.hasFilter()) 297 return null; 298 CodeSystem cs = context.fetchCodeSystem(vsi.getSystem()); 299 if (cs == null) 300 return null; 301 if (vsi.hasConcept()) { 302 for (ConceptReferenceComponent cc : vsi.getConcept()) { 303 boolean match = cs.getCaseSensitive() ? cc.getCode().equals(code) : cc.getCode().equalsIgnoreCase(code); 304 if (match) { 305 if (sys == null) 306 sys = vsi.getSystem(); 307 else if (!sys.equals(vsi.getSystem())) 308 return null; 309 } 310 } 311 } else { 312 ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code); 313 if (cc != null) { 314 if (sys == null) 315 sys = vsi.getSystem(); 316 else if (!sys.equals(vsi.getSystem())) 317 return null; 318 } 319 } 320 } 321 } 322 323 return sys; 324 } 325 326 @Override 327 public boolean codeInValueSet(String system, String code) throws FHIRException { 328 if (valueset.hasExpansion()) { 329 return checkExpansion(new Coding(system, code, null)); 330 } else if (valueset.hasCompose()) { 331 boolean ok = false; 332 for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) { 333 ok = ok || inComponent(vsi, system, code, valueset.getCompose().getInclude().size() == 1); 334 } 335 for (ConceptSetComponent vsi : valueset.getCompose().getExclude()) { 336 ok = ok && !inComponent(vsi, system, code, valueset.getCompose().getInclude().size() == 1); 337 } 338 return ok; 339 } 340 341 return false; 342 } 343 344 private boolean inComponent(ConceptSetComponent vsi, String system, String code, boolean only) throws FHIRException { 345 for (UriType uri : vsi.getValueSet()) { 346 if (inImport(uri.getValue(), system, code)) 347 return true; 348 } 349 350 if (!vsi.hasSystem()) 351 return false; 352 353 if (only && system == null) { 354 // whether we know the system or not, we'll accept the stated codes at face 355 // value 356 for (ConceptReferenceComponent cc : vsi.getConcept()) 357 if (cc.getCode().equals(code)) 358 return true; 359 } 360 361 if (!system.equals(vsi.getSystem())) 362 return false; 363 if (vsi.hasFilter()) { 364 boolean ok = true; 365 for (ConceptSetFilterComponent f : vsi.getFilter()) 366 if (!codeInFilter(system, f, code)) { 367 ok = false; 368 break; 369 } 370 if (ok) 371 return true; 372 } 373 374 CodeSystem def = context.fetchCodeSystem(system); 375 if (def.getContent() != CodeSystemContentMode.COMPLETE) 376 throw new FHIRException("Unable to resolve system " + vsi.getSystem() + " - system is not complete"); 377 378 List<ConceptDefinitionComponent> list = def.getConcept(); 379 boolean ok = validateCodeInConceptList(code, def, list); 380 if (ok && vsi.hasConcept()) { 381 for (ConceptReferenceComponent cc : vsi.getConcept()) 382 if (cc.getCode().equals(code)) 383 return true; 384 return false; 385 } else 386 return ok; 387 } 388 389 private boolean codeInFilter(String system, ConceptSetFilterComponent f, String code) throws FHIRException { 390 CodeSystem cs = context.fetchCodeSystem(system); 391 if (cs == null) 392 throw new FHIRException("Unable to evaluate filters on unknown code system '" + system + "'"); 393 if ("concept".equals(f.getProperty())) 394 return codeInConceptFilter(cs, f, code); 395 else { 396 log.error("todo: handle filters with property = " + f.getProperty()); 397 throw new FHIRException("Unable to handle system " + cs.getUrl() + " filter with property = " + f.getProperty()); 398 } 399 } 400 401 private boolean codeInConceptFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) throws FHIRException { 402 switch (f.getOp()) { 403 case ISA: 404 return codeInConceptIsAFilter(cs, f, code); 405 case ISNOTA: 406 return !codeInConceptIsAFilter(cs, f, code); 407 default: 408 log.error("todo: handle concept filters with op = " + f.getOp()); 409 throw new FHIRException("Unable to handle system " + cs.getUrl() + " concept filter with op = " + f.getOp()); 410 } 411 } 412 413 private boolean codeInConceptIsAFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) { 414 if (code.equals(f.getProperty())) 415 return true; 416 ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), f.getValue()); 417 if (cc == null) 418 return false; 419 cc = findCodeInConcept(cc.getConcept(), code); 420 return cc != null; 421 } 422 423 public boolean validateCodeInConceptList(String code, CodeSystem def, List<ConceptDefinitionComponent> list) { 424 if (def.getCaseSensitive()) { 425 for (ConceptDefinitionComponent cc : list) { 426 if (cc.getCode().equals(code)) 427 return true; 428 if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept())) 429 return true; 430 } 431 } else { 432 for (ConceptDefinitionComponent cc : list) { 433 if (cc.getCode().equalsIgnoreCase(code)) 434 return true; 435 if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept())) 436 return true; 437 } 438 } 439 return false; 440 } 441 442 private ValueSetCheckerSimple getVs(String url) { 443 if (inner.containsKey(url)) { 444 return inner.get(url); 445 } 446 ValueSet vs = context.fetchResource(ValueSet.class, url); 447 ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, context); 448 inner.put(url, vsc); 449 return vsc; 450 } 451 452 private boolean inImport(String uri, String system, String code) throws FHIRException { 453 return getVs(uri).codeInValueSet(system, code); 454 } 455 456}