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