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