
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 org.hl7.fhir.exceptions.FHIRException; 038import org.hl7.fhir.r4.context.IWorkerContext; 039import org.hl7.fhir.r4.context.IWorkerContext.ValidationResult; 040import org.hl7.fhir.r4.model.CanonicalType; 041import org.hl7.fhir.r4.model.CodeSystem; 042import org.hl7.fhir.r4.model.CodeSystem.CodeSystemContentMode; 043import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent; 044import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionDesignationComponent; 045import org.hl7.fhir.r4.model.CodeableConcept; 046import org.hl7.fhir.r4.model.Coding; 047import org.hl7.fhir.r4.model.UriType; 048import org.hl7.fhir.r4.model.ValueSet; 049import org.hl7.fhir.r4.model.ValueSet.ConceptReferenceComponent; 050import org.hl7.fhir.r4.model.ValueSet.ConceptReferenceDesignationComponent; 051import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent; 052import org.hl7.fhir.r4.model.ValueSet.ConceptSetFilterComponent; 053import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent; 054import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 055import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage; 056import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 057import org.hl7.fhir.utilities.validation.ValidationOptions; 058 059@MarkedToMoveToAdjunctPackage 060public class ValueSetCheckerSimple implements ValueSetChecker { 061 062 private ValueSet valueset; 063 private IWorkerContext context; 064 private Map<String, ValueSetCheckerSimple> inner = new HashMap<>(); 065 private ValidationOptions options; 066 067 public ValueSetCheckerSimple(ValidationOptions options, ValueSet source, IWorkerContext context) { 068 this.valueset = source; 069 this.context = context; 070 this.options = options; 071 } 072 073 public ValidationResult validateCode(CodeableConcept code) throws FHIRException { 074 // first, we validate the codings themselves 075 List<String> errors = new ArrayList<String>(); 076 List<String> warnings = new ArrayList<String>(); 077 for (Coding c : code.getCoding()) { 078 if (!c.hasSystem()) 079 warnings.add("Coding has no system"); 080 CodeSystem cs = context.fetchCodeSystem(c.getSystem()); 081 if (cs == null) 082 warnings.add("Unsupported system " + c.getSystem() + " - system is not specified or implicit"); 083 else if (cs.getContent() != CodeSystemContentMode.COMPLETE) 084 warnings.add("Unable to resolve system " + c.getSystem() + " - system is not complete"); 085 else { 086 ValidationResult res = validateCode(c, cs); 087 if (!res.isOk()) 088 errors.add(res.getMessage()); 089 else if (res.getMessage() != null) 090 warnings.add(res.getMessage()); 091 } 092 } 093 if (valueset != null) { 094 boolean ok = false; 095 for (Coding c : code.getCoding()) { 096 ok = ok || codeInValueSet(c.getSystem(), c.getCode()); 097 } 098 if (!ok) 099 errors.add(0, "None of the provided codes are in the value set " + valueset.getUrl()); 100 } 101 if (errors.size() > 0) 102 return new ValidationResult(IssueSeverity.ERROR, errors.toString()); 103 else if (warnings.size() > 0) 104 return new ValidationResult(IssueSeverity.WARNING, warnings.toString()); 105 else 106 return new ValidationResult(IssueSeverity.INFORMATION, null); 107 } 108 109 public ValidationResult validateCode(Coding code) throws FHIRException { 110 String warningMessage = null; 111 // first, we validate the concept itself 112 113 String system = code.hasSystem() ? code.getSystem() : getValueSetSystem(); 114 if (system == null && !code.hasDisplay()) { // dealing with just a plain code (enum) 115 system = systemForCodeInValueSet(code.getCode()); 116 } 117 if (!code.hasSystem()) 118 code.setSystem(system); 119 boolean inExpansion = checkExpansion(code); 120 CodeSystem cs = context.fetchCodeSystem(system); 121 if (cs == null) { 122 warningMessage = "Unable to resolve system " + system + " - system is not specified or implicit"; 123 if (!inExpansion) 124 throw new FHIRException(warningMessage); 125 } 126 if (cs != null && cs.getContent() != CodeSystemContentMode.COMPLETE) { 127 warningMessage = "Unable to resolve system " + system + " - system is not complete"; 128 if (!inExpansion) 129 throw new FHIRException(warningMessage); 130 } 131 132 ValidationResult res = null; 133 if (cs != null) 134 res = validateCode(code, cs); 135 136 // then, if we have a value set, we check it's in the value set 137 if ((res == null || res.isOk()) && valueset != null && !codeInValueSet(system, code.getCode())) { 138 if (!inExpansion) 139 res.setMessage("Not in value set " + valueset.getUrl()).setSeverity(IssueSeverity.ERROR); 140 else if (warningMessage != null) 141 res = new ValidationResult(IssueSeverity.WARNING, "Code found in expansion, however: " + warningMessage); 142 else 143 res.setMessage("Code found in expansion, however: " + res.getMessage()); 144 } 145 return res; 146 } 147 148 boolean checkExpansion(Coding code) { 149 if (valueset == null || !valueset.hasExpansion()) 150 return false; 151 return checkExpansion(code, valueset.getExpansion().getContains()); 152 } 153 154 boolean checkExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) { 155 for (ValueSetExpansionContainsComponent containsComponent : contains) { 156 if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) 157 return true; 158 if (containsComponent.hasContains() && checkExpansion(code, containsComponent.getContains())) 159 return true; 160 } 161 return false; 162 } 163 164 private ValidationResult validateCode(Coding code, CodeSystem cs) { 165 ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code.getCode()); 166 if (cc == null) 167 return new ValidationResult(IssueSeverity.ERROR, "Unknown Code " + gen(code) + " in " + cs.getUrl()); 168 if (code.getDisplay() == null) 169 return new ValidationResult(cc); 170 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 171 if (cc.hasDisplay()) { 172 b.append(cc.getDisplay()); 173 if (code.getDisplay().equalsIgnoreCase(cc.getDisplay())) 174 return new ValidationResult(cc); 175 } 176 for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) { 177 b.append(ds.getValue()); 178 if (code.getDisplay().equalsIgnoreCase(ds.getValue())) 179 return new ValidationResult(cc); 180 } 181 // also check to see if the value set has another display 182 ConceptReferenceComponent vs = findValueSetRef(code.getSystem(), code.getCode()); 183 if (vs != null && (vs.hasDisplay() || vs.hasDesignation())) { 184 if (vs.hasDisplay()) { 185 b.append(vs.getDisplay()); 186 if (code.getDisplay().equalsIgnoreCase(vs.getDisplay())) 187 return new ValidationResult(cc); 188 } 189 for (ConceptReferenceDesignationComponent ds : vs.getDesignation()) { 190 b.append(ds.getValue()); 191 if (code.getDisplay().equalsIgnoreCase(ds.getValue())) 192 return new ValidationResult(cc); 193 } 194 } 195 return new ValidationResult(IssueSeverity.WARNING, "Display Name for " + code.getSystem() + "#" + code.getCode() 196 + " should be one of '" + b.toString() + "' instead of " + code.getDisplay(), cc); 197 } 198 199 private ConceptReferenceComponent findValueSetRef(String system, String code) { 200 if (valueset == null) 201 return null; 202 // if it has an expansion 203 for (ValueSetExpansionContainsComponent exp : valueset.getExpansion().getContains()) { 204 if (system.equals(exp.getSystem()) && code.equals(exp.getCode())) { 205 ConceptReferenceComponent cc = new ConceptReferenceComponent(); 206 cc.setDisplay(exp.getDisplay()); 207 cc.setDesignation(exp.getDesignation()); 208 return cc; 209 } 210 } 211 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 212 if (system.equals(inc.getSystem())) { 213 for (ConceptReferenceComponent cc : inc.getConcept()) { 214 if (cc.getCode().equals(code)) 215 return cc; 216 } 217 } 218 for (CanonicalType url : inc.getValueSet()) { 219 ConceptReferenceComponent cc = getVs(url.asStringValue()).findValueSetRef(system, code); 220 if (cc != null) 221 return cc; 222 } 223 } 224 return null; 225 } 226 227 private String gen(Coding code) { 228 if (code.hasSystem()) 229 return code.getSystem() + "#" + code.getCode(); 230 else 231 return null; 232 } 233 234 private String getValueSetSystem() throws FHIRException { 235 if (valueset == null) 236 throw new FHIRException("Unable to resolve system - no value set"); 237 if (valueset.getCompose().getInclude().size() == 0) { 238 if (!valueset.hasExpansion() || valueset.getExpansion().getContains().size() == 0) 239 throw new FHIRException("Unable to resolve system - value set has no includes or expansion"); 240 else { 241 String cs = valueset.getExpansion().getContains().get(0).getSystem(); 242 if (cs != null && checkSystem(valueset.getExpansion().getContains(), cs)) 243 return cs; 244 else 245 throw new FHIRException("Unable to resolve system - value set expansion has multiple systems"); 246 } 247 } 248 for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { 249 if (inc.hasValueSet()) 250 throw new FHIRException("Unable to resolve system - value set has imports"); 251 if (!inc.hasSystem()) 252 throw new FHIRException("Unable to resolve system - value set has include with no system"); 253 } 254 if (valueset.getCompose().getInclude().size() == 1) 255 return valueset.getCompose().getInclude().get(0).getSystem(); 256 257 return null; 258 } 259 260 /* 261 * Check that all system values within an expansion correspond to the specified 262 * system value 263 */ 264 private boolean checkSystem(List<ValueSetExpansionContainsComponent> containsList, String system) { 265 for (ValueSetExpansionContainsComponent contains : containsList) { 266 if (!contains.getSystem().equals(system) 267 || (contains.hasContains() && !checkSystem(contains.getContains(), system))) 268 return false; 269 } 270 return true; 271 } 272 273 private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code) { 274 for (ConceptDefinitionComponent cc : concept) { 275 if (code.equals(cc.getCode())) 276 return cc; 277 ConceptDefinitionComponent c = findCodeInConcept(cc.getConcept(), code); 278 if (c != null) 279 return c; 280 } 281 return null; 282 } 283 284 private String systemForCodeInValueSet(String code) { 285 String sys = null; 286 if (valueset.hasCompose()) { 287 if (valueset.getCompose().hasExclude()) 288 return null; 289 for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) { 290 if (vsi.hasValueSet()) 291 return null; 292 if (!vsi.hasSystem()) 293 return null; 294 if (vsi.hasFilter()) 295 return null; 296 CodeSystem cs = context.fetchCodeSystem(vsi.getSystem()); 297 if (cs == null) 298 return null; 299 if (vsi.hasConcept()) { 300 for (ConceptReferenceComponent cc : vsi.getConcept()) { 301 boolean match = cs.getCaseSensitive() ? cc.getCode().equals(code) : cc.getCode().equalsIgnoreCase(code); 302 if (match) { 303 if (sys == null) 304 sys = vsi.getSystem(); 305 else if (!sys.equals(vsi.getSystem())) 306 return null; 307 } 308 } 309 } else { 310 ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code); 311 if (cc != null) { 312 if (sys == null) 313 sys = vsi.getSystem(); 314 else if (!sys.equals(vsi.getSystem())) 315 return null; 316 } 317 } 318 } 319 } 320 321 return sys; 322 } 323 324 @Override 325 public boolean codeInValueSet(String system, String code) throws FHIRException { 326 if (valueset.hasExpansion()) { 327 return checkExpansion(new Coding(system, code, null)); 328 } else if (valueset.hasCompose()) { 329 boolean ok = false; 330 for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) { 331 ok = ok || inComponent(vsi, system, code, valueset.getCompose().getInclude().size() == 1); 332 } 333 for (ConceptSetComponent vsi : valueset.getCompose().getExclude()) { 334 ok = ok && !inComponent(vsi, system, code, valueset.getCompose().getInclude().size() == 1); 335 } 336 return ok; 337 } 338 339 return false; 340 } 341 342 private boolean inComponent(ConceptSetComponent vsi, String system, String code, boolean only) throws FHIRException { 343 for (UriType uri : vsi.getValueSet()) { 344 if (inImport(uri.getValue(), system, code)) 345 return true; 346 } 347 348 if (!vsi.hasSystem()) 349 return false; 350 351 if (only && system == null) { 352 // whether we know the system or not, we'll accept the stated codes at face 353 // value 354 for (ConceptReferenceComponent cc : vsi.getConcept()) 355 if (cc.getCode().equals(code)) 356 return true; 357 } 358 359 if (!system.equals(vsi.getSystem())) 360 return false; 361 if (vsi.hasFilter()) { 362 boolean ok = true; 363 for (ConceptSetFilterComponent f : vsi.getFilter()) 364 if (!codeInFilter(system, f, code)) { 365 ok = false; 366 break; 367 } 368 if (ok) 369 return true; 370 } 371 372 CodeSystem def = context.fetchCodeSystem(system); 373 if (def.getContent() != CodeSystemContentMode.COMPLETE) 374 throw new FHIRException("Unable to resolve system " + vsi.getSystem() + " - system is not complete"); 375 376 List<ConceptDefinitionComponent> list = def.getConcept(); 377 boolean ok = validateCodeInConceptList(code, def, list); 378 if (ok && vsi.hasConcept()) { 379 for (ConceptReferenceComponent cc : vsi.getConcept()) 380 if (cc.getCode().equals(code)) 381 return true; 382 return false; 383 } else 384 return ok; 385 } 386 387 private boolean codeInFilter(String system, ConceptSetFilterComponent f, String code) throws FHIRException { 388 CodeSystem cs = context.fetchCodeSystem(system); 389 if (cs == null) 390 throw new FHIRException("Unable to evaluate filters on unknown code system '" + system + "'"); 391 if ("concept".equals(f.getProperty())) 392 return codeInConceptFilter(cs, f, code); 393 else { 394 System.out.println("todo: handle filters with property = " + f.getProperty()); 395 throw new FHIRException("Unable to handle system " + cs.getUrl() + " filter with property = " + f.getProperty()); 396 } 397 } 398 399 private boolean codeInConceptFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) throws FHIRException { 400 switch (f.getOp()) { 401 case ISA: 402 return codeInConceptIsAFilter(cs, f, code); 403 case ISNOTA: 404 return !codeInConceptIsAFilter(cs, f, code); 405 default: 406 System.out.println("todo: handle concept filters with op = " + f.getOp()); 407 throw new FHIRException("Unable to handle system " + cs.getUrl() + " concept filter with op = " + f.getOp()); 408 } 409 } 410 411 private boolean codeInConceptIsAFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) { 412 if (code.equals(f.getProperty())) 413 return true; 414 ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), f.getValue()); 415 if (cc == null) 416 return false; 417 cc = findCodeInConcept(cc.getConcept(), code); 418 return cc != null; 419 } 420 421 public boolean validateCodeInConceptList(String code, CodeSystem def, List<ConceptDefinitionComponent> list) { 422 if (def.getCaseSensitive()) { 423 for (ConceptDefinitionComponent cc : list) { 424 if (cc.getCode().equals(code)) 425 return true; 426 if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept())) 427 return true; 428 } 429 } else { 430 for (ConceptDefinitionComponent cc : list) { 431 if (cc.getCode().equalsIgnoreCase(code)) 432 return true; 433 if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept())) 434 return true; 435 } 436 } 437 return false; 438 } 439 440 private ValueSetCheckerSimple getVs(String url) { 441 if (inner.containsKey(url)) { 442 return inner.get(url); 443 } 444 ValueSet vs = context.fetchResource(ValueSet.class, url); 445 ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, context); 446 inner.put(url, vsc); 447 return vsc; 448 } 449 450 private boolean inImport(String uri, String system, String code) throws FHIRException { 451 return getVs(uri).codeInValueSet(system, code); 452 } 453 454}