001package org.hl7.fhir.dstu2.utils; 002 003/*- 004 * #%L 005 * org.hl7.fhir.dstu2 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 java.io.FileNotFoundException; 025import java.io.IOException; 026import java.util.ArrayList; 027import java.util.HashMap; 028import java.util.HashSet; 029import java.util.List; 030import java.util.Map; 031import java.util.Set; 032 033import org.hl7.fhir.dstu2.model.BooleanType; 034import org.hl7.fhir.dstu2.model.CodeableConcept; 035import org.hl7.fhir.dstu2.model.Coding; 036import org.hl7.fhir.dstu2.model.ConceptMap; 037import org.hl7.fhir.dstu2.model.Conformance; 038import org.hl7.fhir.dstu2.model.Extension; 039import org.hl7.fhir.dstu2.model.Parameters; 040import org.hl7.fhir.dstu2.model.Parameters.ParametersParameterComponent; 041import org.hl7.fhir.dstu2.model.Reference; 042import org.hl7.fhir.dstu2.model.StringType; 043import org.hl7.fhir.dstu2.model.StructureDefinition; 044import org.hl7.fhir.dstu2.model.UriType; 045import org.hl7.fhir.dstu2.model.ValueSet; 046import org.hl7.fhir.dstu2.model.ValueSet.ConceptDefinitionComponent; 047import org.hl7.fhir.dstu2.model.ValueSet.ConceptDefinitionDesignationComponent; 048import org.hl7.fhir.dstu2.model.ValueSet.ConceptSetComponent; 049import org.hl7.fhir.dstu2.model.ValueSet.ValueSetComposeComponent; 050import org.hl7.fhir.dstu2.model.ValueSet.ValueSetExpansionComponent; 051import org.hl7.fhir.dstu2.model.ValueSet.ValueSetExpansionContainsComponent; 052import org.hl7.fhir.dstu2.terminologies.ValueSetExpander.ETooCostly; 053import org.hl7.fhir.dstu2.terminologies.ValueSetExpander.ValueSetExpansionOutcome; 054import org.hl7.fhir.dstu2.terminologies.ValueSetExpanderFactory; 055import org.hl7.fhir.dstu2.terminologies.ValueSetExpansionCache; 056import org.hl7.fhir.dstu2.utils.client.FHIRToolingClient; 057import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 058import org.hl7.fhir.utilities.Utilities; 059import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 060 061public abstract class BaseWorkerContext implements IWorkerContext { 062 063 // all maps are to the full URI 064 protected Map<String, ValueSet> codeSystems = new HashMap<String, ValueSet>(); 065 protected Map<String, ValueSet> valueSets = new HashMap<String, ValueSet>(); 066 protected Map<String, ConceptMap> maps = new HashMap<String, ConceptMap>(); 067 068 protected ValueSetExpanderFactory expansionCache = new ValueSetExpansionCache(this); 069 protected boolean cacheValidation; // if true, do an expansion and cache the expansion 070 private Set<String> failed = new HashSet<String>(); // value sets for which we don't try to do expansion, since the first attempt to get a comprehensive expansion was not successful 071 protected Map<String, Map<String, ValidationResult>> validationCache = new HashMap<String, Map<String,ValidationResult>>(); 072 073 // private ValueSetExpansionCache expansionCache; // 074 075 protected FHIRToolingClient txServer; 076 077 @Override 078 public ValueSet fetchCodeSystem(String system) { 079 return codeSystems.get(system); 080 } 081 082 @Override 083 public boolean supportsSystem(String system) { 084 if (codeSystems.containsKey(system)) 085 return true; 086 else { 087 Conformance conf = txServer.getConformanceStatement(); 088 for (Extension ex : ToolingExtensions.getExtensions(conf, "http://hl7.org/fhir/StructureDefinition/conformance-supported-system")) { 089 if (system.equals(((UriType) ex.getValue()).getValue())) { 090 return true; 091 } 092 } 093 } 094 return false; 095 } 096 097 @Override 098 public ValueSetExpansionOutcome expandVS(ValueSet vs, boolean cacheOk) { 099 try { 100 Map<String, String> params = new HashMap<String, String>(); 101 params.put("_limit", "10000"); 102 params.put("_incomplete", "true"); 103 params.put("profile", "http://www.healthintersections.com.au/fhir/expansion/no-details"); 104 ValueSet result = txServer.expandValueset(vs, null, params); 105 return new ValueSetExpansionOutcome(result); 106 } catch (Exception e) { 107 return new ValueSetExpansionOutcome("Error expanding ValueSet \""+vs.getUrl()+": "+e.getMessage()); 108 } 109 } 110 111 private ValidationResult handleByCache(ValueSet vs, Coding coding, boolean tryCache) { 112 String cacheId = cacheId(coding); 113 Map<String, ValidationResult> cache = validationCache.get(vs.getUrl()); 114 if (cache == null) { 115 cache = new HashMap<String, IWorkerContext.ValidationResult>(); 116 validationCache.put(vs.getUrl(), cache); 117 } 118 if (cache.containsKey(cacheId)) 119 return cache.get(cacheId); 120 if (!tryCache) 121 return null; 122 if (!cacheValidation) 123 return null; 124 if (failed.contains(vs.getUrl())) 125 return null; 126 ValueSetExpansionOutcome vse = expandVS(vs, true); 127 if (vse.getValueset() == null || notcomplete(vse.getValueset())) { 128 failed.add(vs.getUrl()); 129 return null; 130 } 131 132 ValidationResult res = validateCode(coding, vse.getValueset()); 133 cache.put(cacheId, res); 134 return res; 135 } 136 137 private boolean notcomplete(ValueSet vs) { 138 if (!vs.hasExpansion()) 139 return true; 140 if (!vs.getExpansion().getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/valueset-unclosed").isEmpty()) 141 return true; 142 if (!vs.getExpansion().getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/valueset-toocostly").isEmpty()) 143 return true; 144 return false; 145 } 146 147 private ValidationResult handleByCache(ValueSet vs, CodeableConcept concept, boolean tryCache) { 148 String cacheId = cacheId(concept); 149 Map<String, ValidationResult> cache = validationCache.get(vs.getUrl()); 150 if (cache == null) { 151 cache = new HashMap<String, IWorkerContext.ValidationResult>(); 152 validationCache.put(vs.getUrl(), cache); 153 } 154 if (cache.containsKey(cacheId)) 155 return cache.get(cacheId); 156 157 if (validationCache.containsKey(vs.getUrl()) && validationCache.get(vs.getUrl()).containsKey(cacheId)) 158 return validationCache.get(vs.getUrl()).get(cacheId); 159 if (!tryCache) 160 return null; 161 if (!cacheValidation) 162 return null; 163 if (failed.contains(vs.getUrl())) 164 return null; 165 ValueSetExpansionOutcome vse = expandVS(vs, true); 166 if (vse.getValueset() == null || notcomplete(vse.getValueset())) { 167 failed.add(vs.getUrl()); 168 return null; 169 } 170 ValidationResult res = validateCode(concept, vse.getValueset()); 171 cache.put(cacheId, res); 172 return res; 173 } 174 175 private String cacheId(Coding coding) { 176 return "|"+coding.getSystem()+"|"+coding.getVersion()+"|"+coding.getCode()+"|"+coding.getDisplay(); 177 } 178 179 private String cacheId(CodeableConcept cc) { 180 StringBuilder b = new StringBuilder(); 181 for (Coding c : cc.getCoding()) { 182 b.append("#"); 183 b.append(cacheId(c)); 184 } 185 return b.toString(); 186 } 187 188 private ValidationResult verifyCodeExternal(ValueSet vs, Coding coding, boolean tryCache) { 189 ValidationResult res = handleByCache(vs, coding, tryCache); 190 if (res != null) 191 return res; 192 Parameters pin = new Parameters(); 193 pin.addParameter().setName("coding").setValue(coding); 194 pin.addParameter().setName("valueSet").setResource(vs); 195 res = serverValidateCode(pin); 196 Map<String, ValidationResult> cache = validationCache.get(vs.getUrl()); 197 cache.put(cacheId(coding), res); 198 return res; 199 } 200 201 private ValidationResult verifyCodeExternal(ValueSet vs, CodeableConcept cc, boolean tryCache) { 202 ValidationResult res = handleByCache(vs, cc, tryCache); 203 if (res != null) 204 return res; 205 Parameters pin = new Parameters(); 206 pin.addParameter().setName("codeableConcept").setValue(cc); 207 pin.addParameter().setName("valueSet").setResource(vs); 208 res = serverValidateCode(pin); 209 Map<String, ValidationResult> cache = validationCache.get(vs.getUrl()); 210 cache.put(cacheId(cc), res); 211 return res; 212 } 213 214 private ValidationResult serverValidateCode(Parameters pin) { 215 Parameters pout = txServer.operateType(ValueSet.class, "validate-code", pin); 216 boolean ok = false; 217 String message = "No Message returned"; 218 String display = null; 219 for (ParametersParameterComponent p : pout.getParameter()) { 220 if (p.getName().equals("result")) 221 ok = ((BooleanType) p.getValue()).getValue().booleanValue(); 222 else if (p.getName().equals("message")) 223 message = ((StringType) p.getValue()).getValue(); 224 else if (p.getName().equals("display")) 225 display = ((StringType) p.getValue()).getValue(); 226 } 227 if (!ok) 228 return new ValidationResult(IssueSeverity.ERROR, message); 229 else if (display != null) 230 return new ValidationResult(new ConceptDefinitionComponent().setDisplay(display)); 231 else 232 return new ValidationResult(null); 233 } 234 235 236 @Override 237 public ValueSetExpansionComponent expandVS(ConceptSetComponent inc) { 238 ValueSet vs = new ValueSet(); 239 vs.setCompose(new ValueSetComposeComponent()); 240 vs.getCompose().getInclude().add(inc); 241 ValueSetExpansionOutcome vse = expandVS(vs, true); 242 return vse.getValueset().getExpansion(); 243 } 244 245 @Override 246 public ValidationResult validateCode(String system, String code, String display) { 247 try { 248 if (codeSystems.containsKey(system)) 249 return verifyCodeInternal(codeSystems.get(system), system, code, display); 250 else 251 return verifyCodeExternal(null, new Coding().setSystem(system).setCode(code).setDisplay(display), true); 252 } catch (Exception e) { 253 return new ValidationResult(IssueSeverity.FATAL, "Error validating code \""+code+"\" in system \""+system+"\": "+e.getMessage()); 254 } 255 } 256 257 258 @Override 259 public ValidationResult validateCode(Coding code, ValueSet vs) { 260 try { 261 if (codeSystems.containsKey(code.getSystem()) || vs.hasExpansion()) 262 return verifyCodeInternal(codeSystems.get(code.getSystem()), code.getSystem(), code.getCode(), code.getDisplay()); 263 else 264 return verifyCodeExternal(vs, code, true); 265 } catch (Exception e) { 266 return new ValidationResult(IssueSeverity.FATAL, "Error validating code \""+code+"\" in system \""+code.getSystem()+"\": "+e.getMessage()); 267 } 268 } 269 270 @Override 271 public ValidationResult validateCode(CodeableConcept code, ValueSet vs) { 272 try { 273 if (vs.hasCodeSystem() || vs.hasExpansion()) 274 return verifyCodeInternal(vs, code); 275 else 276 return verifyCodeExternal(vs, code, true); 277 } catch (Exception e) { 278 return new ValidationResult(IssueSeverity.FATAL, "Error validating code \""+code.toString()+"\": "+e.getMessage()); 279 } 280 } 281 282 283 @Override 284 public ValidationResult validateCode(String system, String code, String display, ValueSet vs) { 285 try { 286 if (system == null && vs.hasCodeSystem()) 287 return verifyCodeInternal(vs, vs.getCodeSystem().getSystem(), code, display); 288 else if (codeSystems.containsKey(system) || vs.hasExpansion()) 289 return verifyCodeInternal(vs, system, code, display); 290 else 291 return verifyCodeExternal(vs, new Coding().setSystem(system).setCode(code).setDisplay(display), true); 292 } catch (Exception e) { 293 return new ValidationResult(IssueSeverity.FATAL, "Error validating code \""+code+"\" in system \""+system+"\": "+e.getMessage()); 294 } 295 } 296 297 @Override 298 public ValidationResult validateCode(String system, String code, String display, ConceptSetComponent vsi) { 299 try { 300 ValueSet vs = new ValueSet().setUrl(Utilities.makeUuidUrn()); 301 vs.getCompose().addInclude(vsi); 302 return verifyCodeExternal(vs, new Coding().setSystem(system).setCode(code).setDisplay(display), true); 303 } catch (Exception e) { 304 return new ValidationResult(IssueSeverity.FATAL, "Error validating code \""+code+"\" in system \""+system+"\": "+e.getMessage()); 305 } 306 } 307 308 @Override 309 public List<ConceptMap> findMapsForSource(String url) { 310 List<ConceptMap> res = new ArrayList<ConceptMap>(); 311 for (ConceptMap map : maps.values()) 312 if (((Reference) map.getSource()).getReference().equals(url)) 313 res.add(map); 314 return res; 315 } 316 317 private ValidationResult verifyCodeInternal(ValueSet vs, CodeableConcept code) throws FileNotFoundException, ETooCostly, IOException { 318 for (Coding c : code.getCoding()) { 319 ValidationResult res = verifyCodeInternal(vs, c.getSystem(), c.getCode(), c.getDisplay()); 320 if (res.isOk()) 321 return res; 322 } 323 if (code.getCoding().isEmpty()) 324 return new ValidationResult(IssueSeverity.ERROR, "None code provided"); 325 else 326 return new ValidationResult(IssueSeverity.ERROR, "None of the codes are in the specified value set"); 327 } 328 329 private ValidationResult verifyCodeInternal(ValueSet vs, String system, String code, String display) throws FileNotFoundException, ETooCostly, IOException { 330 if (vs.hasExpansion()) 331 return verifyCodeInExpansion(vs, system, code, display); 332 else if (vs.hasCodeSystem() && !vs.hasCompose()) 333 return verifyCodeInCodeSystem(vs, system, code, display); 334 else { 335 ValueSetExpansionOutcome vse = expansionCache.getExpander().expand(vs); 336 if (vse.getValueset() != null) 337 return verifyCodeExternal(vs, new Coding().setSystem(system).setCode(code).setDisplay(display), false); 338 else 339 return verifyCodeInExpansion(vse.getValueset(), system, code, display); 340 } 341 } 342 343 private ValidationResult verifyCodeInCodeSystem(ValueSet vs, String system, String code, String display) { 344 ConceptDefinitionComponent cc = findCodeInConcept(vs.getCodeSystem().getConcept(), code); 345 if (cc == null) 346 return new ValidationResult(IssueSeverity.ERROR, "Unknown Code "+code+" in "+vs.getCodeSystem().getSystem()); 347 if (display == null) 348 return new ValidationResult(cc); 349 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 350 if (cc.hasDisplay()) { 351 b.append(cc.getDisplay()); 352 if (display.equalsIgnoreCase(cc.getDisplay())) 353 return new ValidationResult(cc); 354 } 355 for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) { 356 b.append(ds.getValue()); 357 if (display.equalsIgnoreCase(ds.getValue())) 358 return new ValidationResult(cc); 359 } 360 return new ValidationResult(IssueSeverity.ERROR, "Display Name for "+code+" must be one of '"+b.toString()+"'"); 361 } 362 363 364 private ValidationResult verifyCodeInExpansion(ValueSet vs, String system,String code, String display) { 365 ValueSetExpansionContainsComponent cc = findCode(vs.getExpansion().getContains(), code); 366 if (cc == null) 367 return new ValidationResult(IssueSeverity.ERROR, "Unknown Code "+code+" in "+vs.getCodeSystem().getSystem()); 368 if (display == null) 369 return new ValidationResult(new ConceptDefinitionComponent().setCode(code).setDisplay(cc.getDisplay())); 370 if (cc.hasDisplay()) { 371 if (display.equalsIgnoreCase(cc.getDisplay())) 372 return new ValidationResult(new ConceptDefinitionComponent().setCode(code).setDisplay(cc.getDisplay())); 373 return new ValidationResult(IssueSeverity.ERROR, "Display Name for "+code+" must be '"+cc.getDisplay()+"'"); 374 } 375 return null; 376 } 377 378 private ValueSetExpansionContainsComponent findCode(List<ValueSetExpansionContainsComponent> contains, String code) { 379 for (ValueSetExpansionContainsComponent cc : contains) { 380 if (code.equals(cc.getCode())) 381 return cc; 382 ValueSetExpansionContainsComponent c = findCode(cc.getContains(), code); 383 if (c != null) 384 return c; 385 } 386 return null; 387 } 388 389 private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code) { 390 for (ConceptDefinitionComponent cc : concept) { 391 if (code.equals(cc.getCode())) 392 return cc; 393 ConceptDefinitionComponent c = findCodeInConcept(cc.getConcept(), code); 394 if (c != null) 395 return c; 396 } 397 return null; 398 } 399 400 @Override 401 public StructureDefinition fetchTypeDefinition(String typeName) { 402 return fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+typeName); 403 } 404 405}