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