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