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