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