
001package org.hl7.fhir.dstu3.context; 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.ByteArrayOutputStream; 035import java.io.FileNotFoundException; 036import java.io.FileOutputStream; 037import java.io.IOException; 038import java.util.ArrayList; 039import java.util.HashMap; 040import java.util.HashSet; 041import java.util.List; 042import java.util.Locale; 043import java.util.Map; 044import java.util.ResourceBundle; 045import java.util.Set; 046 047import lombok.extern.slf4j.Slf4j; 048import org.hl7.fhir.dstu3.formats.IParser.OutputStyle; 049import org.hl7.fhir.dstu3.formats.JsonParser; 050import org.hl7.fhir.dstu3.model.BooleanType; 051import org.hl7.fhir.dstu3.model.Bundle; 052import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; 053import org.hl7.fhir.dstu3.model.CodeSystem; 054import org.hl7.fhir.dstu3.model.CodeSystem.CodeSystemHierarchyMeaning; 055import org.hl7.fhir.dstu3.model.CodeSystem.ConceptDefinitionComponent; 056import org.hl7.fhir.dstu3.model.CodeSystem.ConceptDefinitionDesignationComponent; 057import org.hl7.fhir.dstu3.model.CodeableConcept; 058import org.hl7.fhir.dstu3.model.Coding; 059import org.hl7.fhir.dstu3.model.ConceptMap; 060import org.hl7.fhir.dstu3.model.DataElement; 061import org.hl7.fhir.dstu3.model.ExpansionProfile; 062import org.hl7.fhir.dstu3.model.IntegerType; 063import org.hl7.fhir.dstu3.model.OperationDefinition; 064import org.hl7.fhir.dstu3.model.OperationOutcome; 065import org.hl7.fhir.dstu3.model.Parameters; 066import org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent; 067import org.hl7.fhir.dstu3.model.PrimitiveType; 068import org.hl7.fhir.dstu3.model.Questionnaire; 069import org.hl7.fhir.dstu3.model.Reference; 070import org.hl7.fhir.dstu3.model.Resource; 071import org.hl7.fhir.dstu3.model.SearchParameter; 072import org.hl7.fhir.dstu3.model.StringType; 073import org.hl7.fhir.dstu3.model.StructureDefinition; 074import org.hl7.fhir.dstu3.model.StructureDefinition.TypeDerivationRule; 075import org.hl7.fhir.dstu3.model.StructureMap; 076import org.hl7.fhir.dstu3.model.UriType; 077import org.hl7.fhir.dstu3.model.ValueSet; 078import org.hl7.fhir.dstu3.model.ValueSet.ConceptSetComponent; 079import org.hl7.fhir.dstu3.model.ValueSet.ConceptSetFilterComponent; 080import org.hl7.fhir.dstu3.model.ValueSet.ValueSetComposeComponent; 081import org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionComponent; 082import org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionContainsComponent; 083import org.hl7.fhir.dstu3.terminologies.ValueSetExpander.ETooCostly; 084import org.hl7.fhir.dstu3.terminologies.ValueSetExpander.TerminologyServiceErrorClass; 085import org.hl7.fhir.dstu3.terminologies.ValueSetExpander.ValueSetExpansionOutcome; 086import org.hl7.fhir.dstu3.terminologies.ValueSetExpanderFactory; 087import org.hl7.fhir.dstu3.terminologies.ValueSetExpansionCache; 088import org.hl7.fhir.dstu3.utils.ToolingExtensions; 089import org.hl7.fhir.dstu3.utils.client.FHIRToolingClient; 090import org.hl7.fhir.exceptions.FHIRException; 091import org.hl7.fhir.exceptions.NoTerminologyServiceException; 092import org.hl7.fhir.exceptions.TerminologyServiceException; 093import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 094import org.hl7.fhir.utilities.FileUtilities; 095import org.hl7.fhir.utilities.UUIDUtilities; 096import org.hl7.fhir.utilities.Utilities; 097import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; 098import org.hl7.fhir.utilities.i18n.I18nBase; 099import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 100import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; 101 102import com.google.gson.JsonObject; 103import com.google.gson.JsonSyntaxException; 104 105import ca.uhn.fhir.rest.api.Constants; 106 107 108@Deprecated 109@Slf4j 110public abstract class BaseWorkerContext extends I18nBase implements IWorkerContext { 111 112 // all maps are to the full URI 113 protected Map<String, CodeSystem> codeSystems = new HashMap<String, CodeSystem>(); 114 protected Set<String> nonSupportedCodeSystems = new HashSet<String>(); 115 protected Map<String, ValueSet> valueSets = new HashMap<String, ValueSet>(); 116 protected Map<String, ConceptMap> maps = new HashMap<String, ConceptMap>(); 117 protected Map<String, StructureMap> transforms = new HashMap<String, StructureMap>(); 118 protected Map<String, DataElement> dataElements = new HashMap<String, DataElement>(); 119 protected Map<String, StructureDefinition> profiles = new HashMap<String, StructureDefinition>(); 120 protected Map<String, SearchParameter> searchParameters = new HashMap<String, SearchParameter>(); 121 protected Map<String, StructureDefinition> extensionDefinitions = new HashMap<String, StructureDefinition>(); 122 protected Map<String, Questionnaire> questionnaires = new HashMap<String, Questionnaire>(); 123 protected Map<String, OperationDefinition> operations = new HashMap<String, OperationDefinition>(); 124 125 protected ValueSetExpanderFactory expansionCache = new ValueSetExpansionCache(this); 126 protected boolean cacheValidation; // if true, do an expansion and cache the expansion 127 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 128 protected Map<String, Map<String, ValidationResult>> validationCache = new HashMap<String, Map<String, ValidationResult>>(); 129 protected String tsServer; 130 protected String validationCachePath; 131 protected String name; 132 133 // private ValueSetExpansionCache expansionCache; // 134 135 protected FHIRToolingClient txServer; 136 private Bundle bndCodeSystems; 137 private boolean canRunWithoutTerminology; 138 protected boolean allowLoadingDuplicates; 139 protected boolean noTerminologyServer; 140 protected String cache; 141 private int expandCodesLimit = 1000; 142 143 protected ExpansionProfile expProfile; 144 private Locale locale; 145 private ResourceBundle i18Nmessages; 146 147 public Map<String, CodeSystem> getCodeSystems() { 148 return codeSystems; 149 } 150 151 public Map<String, DataElement> getDataElements() { 152 return dataElements; 153 } 154 155 public Map<String, ValueSet> getValueSets() { 156 return valueSets; 157 } 158 159 public Map<String, ConceptMap> getMaps() { 160 return maps; 161 } 162 163 public Map<String, StructureDefinition> getProfiles() { 164 return profiles; 165 } 166 167 public Map<String, StructureDefinition> getExtensionDefinitions() { 168 return extensionDefinitions; 169 } 170 171 public Map<String, Questionnaire> getQuestionnaires() { 172 return questionnaires; 173 } 174 175 public Map<String, OperationDefinition> getOperations() { 176 return operations; 177 } 178 179 public void seeExtensionDefinition(String url, StructureDefinition ed) throws Exception { 180 if (extensionDefinitions.get(ed.getUrl()) != null) { 181 throw new Exception("duplicate extension definition: " + ed.getUrl()); 182 } 183 extensionDefinitions.put(ed.getId(), ed); 184 extensionDefinitions.put(url, ed); 185 extensionDefinitions.put(ed.getUrl(), ed); 186 } 187 188 public void dropExtensionDefinition(String id) { 189 StructureDefinition sd = extensionDefinitions.get(id); 190 extensionDefinitions.remove(id); 191 if (sd != null) { 192 extensionDefinitions.remove(sd.getUrl()); 193 } 194 } 195 196 public void seeQuestionnaire(String url, Questionnaire theQuestionnaire) throws Exception { 197 if (questionnaires.get(theQuestionnaire.getId()) != null) { 198 throw new Exception("duplicate extension definition: " + theQuestionnaire.getId()); 199 } 200 questionnaires.put(theQuestionnaire.getId(), theQuestionnaire); 201 questionnaires.put(url, theQuestionnaire); 202 } 203 204 public void seeOperation(OperationDefinition opd) throws Exception { 205 if (operations.get(opd.getUrl()) != null) { 206 throw new Exception("duplicate extension definition: " + opd.getUrl()); 207 } 208 operations.put(opd.getUrl(), opd); 209 operations.put(opd.getId(), opd); 210 } 211 212 public void seeValueSet(String url, ValueSet vs) throws Exception { 213 if (valueSets.containsKey(vs.getUrl()) && !allowLoadingDuplicates) { 214 throw new Exception("Duplicate value set " + vs.getUrl()); 215 } 216 valueSets.put(vs.getId(), vs); 217 valueSets.put(url, vs); 218 valueSets.put(vs.getUrl(), vs); 219 } 220 221 public void dropValueSet(String id) { 222 ValueSet vs = valueSets.get(id); 223 valueSets.remove(id); 224 if (vs != null) { 225 valueSets.remove(vs.getUrl()); 226 } 227 } 228 229 public void seeCodeSystem(String url, CodeSystem cs) throws FHIRException { 230 if (codeSystems.containsKey(cs.getUrl()) && !allowLoadingDuplicates) { 231 throw new FHIRException("Duplicate code system " + cs.getUrl()); 232 } 233 codeSystems.put(cs.getId(), cs); 234 codeSystems.put(url, cs); 235 codeSystems.put(cs.getUrl(), cs); 236 } 237 238 public void dropCodeSystem(String id) { 239 CodeSystem cs = codeSystems.get(id); 240 codeSystems.remove(id); 241 if (cs != null) { 242 codeSystems.remove(cs.getUrl()); 243 } 244 } 245 246 public void seeProfile(String url, StructureDefinition p) throws Exception { 247 if (profiles.containsKey(p.getUrl())) { 248 throw new Exception("Duplicate Profile " + p.getUrl()); 249 } 250 profiles.put(p.getId(), p); 251 profiles.put(url, p); 252 profiles.put(p.getUrl(), p); 253 } 254 255 public void dropProfile(String id) { 256 StructureDefinition sd = profiles.get(id); 257 profiles.remove(id); 258 if (sd != null) { 259 profiles.remove(sd.getUrl()); 260 } 261 } 262 263 @Override 264 public CodeSystem fetchCodeSystem(String system) { 265 return codeSystems.get(system); 266 } 267 268 @Override 269 public boolean supportsSystem(String system) throws TerminologyServiceException { 270 if (codeSystems.containsKey(system)) { 271 return true; 272 } else if (nonSupportedCodeSystems.contains(system)) { 273 return false; 274 } else if (system.startsWith("http://example.org") || system.startsWith("http://acme.com") 275 || system.startsWith("http://hl7.org/fhir/valueset-") || system.startsWith("urn:oid:")) { 276 return false; 277 } else { 278 if (noTerminologyServer) { 279 return false; 280 } 281 if (bndCodeSystems == null) { 282 try { 283 tlog("Terminology server: Check for supported code systems for " + system); 284 bndCodeSystems = txServer.fetchFeed(txServer.getAddress() 285 + "/CodeSystem?content-mode=not-present&_summary=true&_count=1000"); 286 } catch (Exception e) { 287 if (canRunWithoutTerminology) { 288 noTerminologyServer = true; 289 log.info("==============!! Running without terminology server !!============== (" + e 290 .getMessage() + ")"); 291 return false; 292 } else { 293 throw new TerminologyServiceException(e); 294 } 295 } 296 } 297 if (bndCodeSystems != null) { 298 for (BundleEntryComponent be : bndCodeSystems.getEntry()) { 299 CodeSystem cs = (CodeSystem) be.getResource(); 300 if (!codeSystems.containsKey(cs.getUrl())) { 301 codeSystems.put(cs.getUrl(), null); 302 } 303 } 304 } 305 if (codeSystems.containsKey(system)) { 306 return true; 307 } 308 } 309 nonSupportedCodeSystems.add(system); 310 return false; 311 } 312 313 @Override 314 public ValueSetExpansionOutcome expandVS(ValueSet vs, boolean cacheOk, boolean heirarchical) { 315 try { 316 if (vs.hasExpansion()) { 317 return new ValueSetExpansionOutcome(vs.copy()); 318 } 319 String cacheFn = null; 320 if (cache != null) { 321 cacheFn = Utilities.path(cache, determineCacheId(vs, heirarchical) + ".json"); 322 if (ManagedFileAccess.file(cacheFn).exists()) { 323 return loadFromCache(vs.copy(), cacheFn); 324 } 325 } 326 if (cacheOk && vs.hasUrl()) { 327 if (expProfile == null) { 328 throw new Exception("No ExpansionProfile provided"); 329 } 330 ValueSetExpansionOutcome vse = expansionCache.getExpander() 331 .expand(vs, expProfile.setExcludeNested(!heirarchical)); 332 if (vse.getValueset() != null) { 333 if (cache != null) { 334 FileOutputStream s = ManagedFileAccess.outStream(cacheFn); 335 newJsonParser().compose(ManagedFileAccess.outStream(cacheFn), vse.getValueset()); 336 s.close(); 337 } 338 } 339 return vse; 340 } else { 341 ValueSetExpansionOutcome res = expandOnServer(vs, cacheFn); 342 if (cacheFn != null) { 343 if (res.getValueset() != null) { 344 saveToCache(res.getValueset(), cacheFn); 345 } else { 346 OperationOutcome oo = new OperationOutcome(); 347 oo.addIssue().getDetails().setText(res.getError()); 348 saveToCache(oo, cacheFn); 349 } 350 } 351 return res; 352 } 353 } catch (NoTerminologyServiceException e) { 354 return new ValueSetExpansionOutcome( 355 e.getMessage() == null ? e.getClass().getName() : e.getMessage(), 356 TerminologyServiceErrorClass.NOSERVICE); 357 } catch (Exception e) { 358 return new ValueSetExpansionOutcome( 359 e.getMessage() == null ? e.getClass().getName() : e.getMessage(), 360 TerminologyServiceErrorClass.UNKNOWN); 361 } 362 } 363 364 private ValueSetExpansionOutcome loadFromCache(ValueSet vs, String cacheFn) 365 throws FileNotFoundException, Exception { 366 JsonParser parser = new JsonParser(); 367 Resource r = parser.parse(ManagedFileAccess.inStream(cacheFn)); 368 if (r instanceof OperationOutcome) { 369 return new ValueSetExpansionOutcome( 370 ((OperationOutcome) r).getIssue().get(0).getDetails().getText(), 371 TerminologyServiceErrorClass.UNKNOWN); 372 } else { 373 vs.setExpansion(((ValueSet) r) 374 .getExpansion()); // because what is cached might be from a different value set 375 return new ValueSetExpansionOutcome(vs); 376 } 377 } 378 379 private void saveToCache(Resource res, String cacheFn) throws FileNotFoundException, Exception { 380 JsonParser parser = new JsonParser(); 381 parser.compose(ManagedFileAccess.outStream(cacheFn), res); 382 } 383 384 private String determineCacheId(ValueSet vs, boolean heirarchical) throws Exception { 385 // just the content logical definition is hashed 386 ValueSet vsid = new ValueSet(); 387 vsid.setCompose(vs.getCompose()); 388 JsonParser parser = new JsonParser(); 389 parser.setOutputStyle(OutputStyle.NORMAL); 390 ByteArrayOutputStream b = new ByteArrayOutputStream(); 391 parser.compose(b, vsid); 392 b.close(); 393 String s = new String(b.toByteArray(), Constants.CHARSET_UTF8); 394 // any code systems we can find, we add these too. 395 for (ConceptSetComponent inc : vs.getCompose().getInclude()) { 396 CodeSystem cs = fetchCodeSystem(inc.getSystem()); 397 if (cs != null) { 398 String css = cacheValue(cs); 399 s = s + css; 400 } 401 } 402 s = s + "-" + Boolean.toString(heirarchical); 403 String r = Integer.toString(s.hashCode()); 404 // FileUtilities.stringToFile(s, Utilities.path(cache, r+".id.json")); 405 return r; 406 } 407 408 409 private String cacheValue(CodeSystem cs) throws IOException { 410 CodeSystem csid = new CodeSystem(); 411 csid.setId(cs.getId()); 412 csid.setVersion(cs.getVersion()); 413 csid.setContent(cs.getContent()); 414 csid.setHierarchyMeaning(CodeSystemHierarchyMeaning.GROUPEDBY); 415 for (ConceptDefinitionComponent cc : cs.getConcept()) { 416 csid.getConcept().add(processCSConcept(cc)); 417 } 418 JsonParser parser = new JsonParser(); 419 parser.setOutputStyle(OutputStyle.NORMAL); 420 ByteArrayOutputStream b = new ByteArrayOutputStream(); 421 parser.compose(b, csid); 422 b.close(); 423 return new String(b.toByteArray(), Constants.CHARSET_UTF8); 424 } 425 426 427 private ConceptDefinitionComponent processCSConcept(ConceptDefinitionComponent cc) { 428 ConceptDefinitionComponent ccid = new ConceptDefinitionComponent(); 429 ccid.setCode(cc.getCode()); 430 ccid.setDisplay(cc.getDisplay()); 431 for (ConceptDefinitionComponent cci : cc.getConcept()) { 432 ccid.getConcept().add(processCSConcept(cci)); 433 } 434 return ccid; 435 } 436 437 public ValueSetExpansionOutcome expandOnServer(ValueSet vs, String fn) throws Exception { 438 if (noTerminologyServer) { 439 return new ValueSetExpansionOutcome( 440 "Error expanding ValueSet: running without terminology services", 441 TerminologyServiceErrorClass.NOSERVICE); 442 } 443 if (expProfile == null) { 444 throw new Exception("No ExpansionProfile provided"); 445 } 446 447 try { 448 Parameters params = new Parameters(); 449 params.addParameter().setName("profile").setResource(expProfile.setIncludeDefinition(false)); 450 params.addParameter().setName("_limit").setValue(new IntegerType(expandCodesLimit)); 451 params.addParameter().setName("_incomplete").setValue(new BooleanType("true")); 452 tlog("Terminology Server: $expand on " + getVSSummary(vs)); 453 ValueSet result = txServer.expandValueset(vs, params); 454 return new ValueSetExpansionOutcome(result); 455 } catch (Exception e) { 456 return new ValueSetExpansionOutcome( 457 "Error expanding ValueSet \"" + vs.getUrl() + ": " + e.getMessage(), 458 TerminologyServiceErrorClass.UNKNOWN); 459 } 460 } 461 462 private String getVSSummary(ValueSet vs) { 463 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 464 for (ConceptSetComponent cc : vs.getCompose().getInclude()) { 465 b.append("Include " + getIncSummary(cc)); 466 } 467 for (ConceptSetComponent cc : vs.getCompose().getExclude()) { 468 b.append("Exclude " + getIncSummary(cc)); 469 } 470 return b.toString(); 471 } 472 473 private String getIncSummary(ConceptSetComponent cc) { 474 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 475 for (UriType vs : cc.getValueSet()) { 476 b.append(vs.asStringValue()); 477 } 478 String vsd = 479 b.length() > 0 ? " where the codes are in the value sets (" + b.toString() + ")" : ""; 480 String system = cc.getSystem(); 481 if (cc.hasConcept()) { 482 return Integer.toString(cc.getConcept().size()) + " codes from " + system + vsd; 483 } 484 if (cc.hasFilter()) { 485 String s = ""; 486 for (ConceptSetFilterComponent f : cc.getFilter()) { 487 if (!Utilities.noString(s)) { 488 s = s + " & "; 489 } 490 s = s + f.getProperty() + " " + f.getOp().toCode() + " " + f.getValue(); 491 } 492 return "from " + system + " where " + s + vsd; 493 } 494 return "All codes from " + system + vsd; 495 } 496 497 private ValidationResult handleByCache(ValueSet vs, Coding coding, boolean tryCache) { 498 String cacheId = cacheId(coding); 499 Map<String, ValidationResult> cache = validationCache.get(vs.getUrl()); 500 if (cache == null) { 501 cache = new HashMap<String, IWorkerContext.ValidationResult>(); 502 validationCache.put(vs.getUrl(), cache); 503 } 504 if (cache.containsKey(cacheId)) { 505 return cache.get(cacheId); 506 } 507 if (!tryCache) { 508 return null; 509 } 510 if (!cacheValidation) { 511 return null; 512 } 513 if (failed.contains(vs.getUrl())) { 514 return null; 515 } 516 ValueSetExpansionOutcome vse = expandVS(vs, true, false); 517 if (vse.getValueset() == null || notcomplete(vse.getValueset())) { 518 failed.add(vs.getUrl()); 519 return null; 520 } 521 522 ValidationResult res = validateCode(coding, vse.getValueset()); 523 cache.put(cacheId, res); 524 return res; 525 } 526 527 private boolean notcomplete(ValueSet vs) { 528 if (!vs.hasExpansion()) { 529 return true; 530 } 531 if (!vs.getExpansion() 532 .getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/valueset-unclosed").isEmpty()) { 533 return true; 534 } 535 if (!vs.getExpansion() 536 .getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/valueset-toocostly").isEmpty()) { 537 return true; 538 } 539 return false; 540 } 541 542 private ValidationResult handleByCache(ValueSet vs, CodeableConcept concept, boolean tryCache) { 543 String cacheId = cacheId(concept); 544 Map<String, ValidationResult> cache = validationCache.get(vs.getUrl()); 545 if (cache == null) { 546 cache = new HashMap<String, IWorkerContext.ValidationResult>(); 547 validationCache.put(vs.getUrl(), cache); 548 } 549 if (cache.containsKey(cacheId)) { 550 return cache.get(cacheId); 551 } 552 553 if (validationCache.containsKey(vs.getUrl()) && validationCache.get(vs.getUrl()) 554 .containsKey(cacheId)) { 555 return validationCache.get(vs.getUrl()).get(cacheId); 556 } 557 if (!tryCache) { 558 return null; 559 } 560 if (!cacheValidation) { 561 return null; 562 } 563 if (failed.contains(vs.getUrl())) { 564 return null; 565 } 566 ValueSetExpansionOutcome vse = expandVS(vs, true, false); 567 if (vse.getValueset() == null || notcomplete(vse.getValueset())) { 568 failed.add(vs.getUrl()); 569 return null; 570 } 571 ValidationResult res = validateCode(concept, vse.getValueset()); 572 cache.put(cacheId, res); 573 return res; 574 } 575 576 private String cacheId(Coding coding) { 577 return "|" + coding.getSystem() + "|" + coding.getVersion() + "|" + coding.getCode() + "|" 578 + coding.getDisplay(); 579 } 580 581 private String cacheId(CodeableConcept cc) { 582 StringBuilder b = new StringBuilder(); 583 for (Coding c : cc.getCoding()) { 584 b.append("#"); 585 b.append(cacheId(c)); 586 } 587 return b.toString(); 588 } 589 590 private ValidationResult verifyCodeExternal(ValueSet vs, Coding coding, boolean tryCache) 591 throws Exception { 592 ValidationResult res = vs == null ? null : handleByCache(vs, coding, tryCache); 593 if (res != null) { 594 return res; 595 } 596 Parameters pin = new Parameters(); 597 pin.addParameter().setName("coding").setValue(coding); 598 if (vs != null) { 599 pin.addParameter().setName("valueSet").setResource(vs); 600 } 601 res = serverValidateCode(pin, vs == null); 602 if (vs != null) { 603 Map<String, ValidationResult> cache = validationCache.get(vs.getUrl()); 604 cache.put(cacheId(coding), res); 605 } 606 return res; 607 } 608 609 private ValidationResult verifyCodeExternal(ValueSet vs, CodeableConcept cc, boolean tryCache) 610 throws Exception { 611 ValidationResult res = handleByCache(vs, cc, tryCache); 612 if (res != null) { 613 return res; 614 } 615 Parameters pin = new Parameters(); 616 pin.addParameter().setName("codeableConcept").setValue(cc); 617 pin.addParameter().setName("valueSet").setResource(vs); 618 res = serverValidateCode(pin, false); 619 Map<String, ValidationResult> cache = validationCache.get(vs.getUrl()); 620 cache.put(cacheId(cc), res); 621 return res; 622 } 623 624 private ValidationResult serverValidateCode(Parameters pin, boolean doCache) throws Exception { 625 if (noTerminologyServer) { 626 return new ValidationResult(null, null, TerminologyServiceErrorClass.NOSERVICE); 627 } 628 String cacheName = doCache ? generateCacheName(pin) : null; 629 ValidationResult res = loadFromCache(cacheName); 630 if (res != null) { 631 return res; 632 } 633 tlog("Terminology Server: $validate-code " + describeValidationParameters(pin)); 634 for (ParametersParameterComponent pp : pin.getParameter()) { 635 if (pp.getName().equals("profile")) { 636 throw new Error("Can only specify profile in the context"); 637 } 638 } 639 if (expProfile == null) { 640 throw new Exception("No ExpansionProfile provided"); 641 } 642 pin.addParameter().setName("profile").setResource(expProfile); 643 644 Parameters pout = txServer.operateType(ValueSet.class, "validate-code", pin); 645 boolean ok = false; 646 String message = "No Message returned"; 647 String display = null; 648 TerminologyServiceErrorClass err = TerminologyServiceErrorClass.UNKNOWN; 649 for (ParametersParameterComponent p : pout.getParameter()) { 650 if (p.getName().equals("result")) { 651 ok = ((BooleanType) p.getValue()).getValue().booleanValue(); 652 } else if (p.getName().equals("message")) { 653 message = ((StringType) p.getValue()).getValue(); 654 } else if (p.getName().equals("display")) { 655 display = ((StringType) p.getValue()).getValue(); 656 } else if (p.getName().equals("cause")) { 657 try { 658 IssueType it = IssueType.fromCode(((StringType) p.getValue()).getValue()); 659 if (it == IssueType.UNKNOWN) { 660 err = TerminologyServiceErrorClass.UNKNOWN; 661 } else if (it == IssueType.NOTSUPPORTED) { 662 err = TerminologyServiceErrorClass.VALUESET_UNSUPPORTED; 663 } 664 } catch (FHIRException e) { 665 } 666 } 667 } 668 if (!ok) { 669 res = new ValidationResult(IssueSeverity.ERROR, message, err); 670 } else if (display != null) { 671 res = new ValidationResult(new ConceptDefinitionComponent().setDisplay(display)); 672 } else { 673 res = new ValidationResult(new ConceptDefinitionComponent()); 674 } 675 saveToCache(res, cacheName); 676 return res; 677 } 678 679 680 private void tlog(String msg) { 681 // log(msg); 682 } 683 684 @SuppressWarnings("rawtypes") 685 private String describeValidationParameters(Parameters pin) { 686 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 687 for (ParametersParameterComponent p : pin.getParameter()) { 688 if (p.hasValue() && p.getValue() instanceof PrimitiveType) { 689 b.append(p.getName() + "=" + ((PrimitiveType) p.getValue()).asStringValue()); 690 } else if (p.hasValue() && p.getValue() instanceof Coding) { 691 b.append("system=" + ((Coding) p.getValue()).getSystem()); 692 b.append("code=" + ((Coding) p.getValue()).getCode()); 693 b.append("display=" + ((Coding) p.getValue()).getDisplay()); 694 } else if (p.hasValue() && p.getValue() instanceof CodeableConcept) { 695 if (((CodeableConcept) p.getValue()).hasCoding()) { 696 Coding c = ((CodeableConcept) p.getValue()).getCodingFirstRep(); 697 b.append("system=" + c.getSystem()); 698 b.append("code=" + c.getCode()); 699 b.append("display=" + c.getDisplay()); 700 } else if (((CodeableConcept) p.getValue()).hasText()) { 701 b.append("text=" + ((CodeableConcept) p.getValue()).getText()); 702 } 703 } else if (p.hasResource() && (p.getResource() instanceof ValueSet)) { 704 b.append("valueset=" + getVSSummary((ValueSet) p.getResource())); 705 } 706 } 707 return b.toString(); 708 } 709 710 private ValidationResult loadFromCache(String fn) throws FileNotFoundException, IOException { 711 if (fn == null) { 712 return null; 713 } 714 if (!(ManagedFileAccess.file(fn).exists())) { 715 return null; 716 } 717 String cnt = FileUtilities.fileToString(fn); 718 if (cnt.startsWith("!error: ")) { 719 return new ValidationResult(IssueSeverity.ERROR, cnt.substring(8)); 720 } else if (cnt.startsWith("!warning: ")) { 721 return new ValidationResult(IssueSeverity.ERROR, cnt.substring(10)); 722 } else { 723 return new ValidationResult(new ConceptDefinitionComponent().setDisplay(cnt)); 724 } 725 } 726 727 private void saveToCache(ValidationResult res, String cacheName) throws IOException { 728 if (cacheName == null) { 729 return; 730 } 731 if (res.getDisplay() != null) { 732 FileUtilities.stringToFile(res.getDisplay(), cacheName); 733 } else if (res.getMessage() != null) { 734 if (res.getSeverity() == IssueSeverity.WARNING) { 735 FileUtilities.stringToFile("!warning: " + res.getMessage(), cacheName); 736 } else { 737 FileUtilities.stringToFile("!error: " + res.getMessage(), cacheName); 738 } 739 } 740 } 741 742 private String generateCacheName(Parameters pin) throws IOException { 743 if (cache == null) { 744 return null; 745 } 746 String json = new JsonParser().composeString(pin); 747 return Utilities.path(cache, "vc" + Integer.toString(json.hashCode()) + ".json"); 748 } 749 750 @Override 751 public ValueSetExpansionComponent expandVS(ConceptSetComponent inc, boolean heirachical) 752 throws TerminologyServiceException { 753 ValueSet vs = new ValueSet(); 754 vs.setCompose(new ValueSetComposeComponent()); 755 vs.getCompose().getInclude().add(inc); 756 ValueSetExpansionOutcome vse = expandVS(vs, true, heirachical); 757 ValueSet valueset = vse.getValueset(); 758 if (valueset == null) { 759 throw new TerminologyServiceException("Error Expanding ValueSet: " + vse.getError()); 760 } 761 return valueset.getExpansion(); 762 } 763 764 @Override 765 public ValidationResult validateCode(String system, String code, String display) { 766 try { 767 if (codeSystems.containsKey(system) && codeSystems.get(system) != null) { 768 return verifyCodeInCodeSystem(codeSystems.get(system), system, code, display); 769 } else { 770 return verifyCodeExternal(null, 771 new Coding().setSystem(system).setCode(code).setDisplay(display), false); 772 } 773 } catch (Exception e) { 774 return new ValidationResult(IssueSeverity.FATAL, 775 "Error validating code \"" + code + "\" in system \"" + system + "\": " + e.getMessage()); 776 } 777 } 778 779 780 @Override 781 public ValidationResult validateCode(Coding code, ValueSet vs) { 782 if (codeSystems.containsKey(code.getSystem()) && codeSystems.get(code.getSystem()) != null) { 783 try { 784 return verifyCodeInCodeSystem(codeSystems.get(code.getSystem()), code.getSystem(), 785 code.getCode(), code.getDisplay()); 786 } catch (Exception e) { 787 return new ValidationResult(IssueSeverity.FATAL, 788 "Error validating code \"" + code + "\" in system \"" + code.getSystem() + "\": " + e 789 .getMessage()); 790 } 791 } else if (vs.hasExpansion()) { 792 try { 793 return verifyCodeInternal(vs, code.getSystem(), code.getCode(), code.getDisplay()); 794 } catch (Exception e) { 795 return new ValidationResult(IssueSeverity.FATAL, 796 "Error validating code \"" + code + "\" in system \"" + code.getSystem() + "\": " + e 797 .getMessage()); 798 } 799 } else { 800 try { 801 return verifyCodeExternal(vs, code, true); 802 } catch (Exception e) { 803 return new ValidationResult(IssueSeverity.WARNING, 804 "Error validating code \"" + code + "\" in system \"" + code.getSystem() + "\": " + e 805 .getMessage()); 806 } 807 } 808 } 809 810 @Override 811 public ValidationResult validateCode(CodeableConcept code, ValueSet vs) { 812 try { 813 if (vs.hasExpansion()) { 814 return verifyCodeInternal(vs, code); 815 } else { 816 // we'll try expanding first; if that doesn't work, then we'll just pass it to the server to validate 817 // ... could be a problem if the server doesn't have the code systems we have locally, so we try not to depend on the server 818 try { 819 ValueSetExpansionOutcome vse = expandVS(vs, true, false); 820 if (vse.getValueset() != null && !hasTooCostlyExpansion(vse.getValueset())) { 821 return verifyCodeInternal(vse.getValueset(), code); 822 } 823 } catch (Exception e) { 824 // failed? we'll just try the server 825 } 826 return verifyCodeExternal(vs, code, true); 827 } 828 } catch (Exception e) { 829 return new ValidationResult(IssueSeverity.FATAL, 830 "Error validating code \"" + code.toString() + "\": " + e.getMessage(), 831 TerminologyServiceErrorClass.SERVER_ERROR); 832 } 833 } 834 835 836 private boolean hasTooCostlyExpansion(ValueSet valueset) { 837 return valueset != null && valueset.hasExpansion() && ToolingExtensions 838 .hasExtension(valueset.getExpansion(), 839 "http://hl7.org/fhir/StructureDefinition/valueset-toocostly"); 840 } 841 842 @Override 843 public ValidationResult validateCode(String system, String code, String display, ValueSet vs) { 844 try { 845 if (system == null && display == null) { 846 return verifyCodeInternal(vs, code); 847 } 848 if ((codeSystems.containsKey(system) && codeSystems.get(system) != null) || vs 849 .hasExpansion()) { 850 return verifyCodeInternal(vs, system, code, display); 851 } else { 852 return verifyCodeExternal(vs, 853 new Coding().setSystem(system).setCode(code).setDisplay(display), true); 854 } 855 } catch (Exception e) { 856 return new ValidationResult(IssueSeverity.FATAL, 857 "Error validating code \"" + code + "\" in system \"" + system + "\": " + e.getMessage(), 858 TerminologyServiceErrorClass.SERVER_ERROR); 859 } 860 } 861 862 @Override 863 public ValidationResult validateCode(String system, String code, String display, 864 ConceptSetComponent vsi) { 865 try { 866 ValueSet vs = new ValueSet(); 867 vs.setUrl(UUIDUtilities.makeUuidUrn()); 868 vs.getCompose().addInclude(vsi); 869 return verifyCodeExternal(vs, 870 new Coding().setSystem(system).setCode(code).setDisplay(display), true); 871 } catch (Exception e) { 872 return new ValidationResult(IssueSeverity.FATAL, 873 "Error validating code \"" + code + "\" in system \"" + system + "\": " + e.getMessage()); 874 } 875 } 876 877 public void initTS(String cachePath, String tsServer) throws Exception { 878 cache = cachePath; 879 this.tsServer = tsServer; 880 expansionCache = new ValueSetExpansionCache(this, null); 881 validationCachePath = Utilities.path(cachePath, "validation.cache"); 882 try { 883 loadValidationCache(); 884 } catch (Exception e) { 885 e.printStackTrace(); 886 } 887 } 888 889 protected void loadValidationCache() throws JsonSyntaxException, Exception { 890 } 891 892 @Override 893 public List<ConceptMap> findMapsForSource(String url) { 894 List<ConceptMap> res = new ArrayList<ConceptMap>(); 895 for (ConceptMap map : maps.values()) { 896 if (((Reference) map.getSource()).getReference().equals(url)) { 897 res.add(map); 898 } 899 } 900 return res; 901 } 902 903 private ValidationResult verifyCodeInternal(ValueSet vs, CodeableConcept code) throws Exception { 904 for (Coding c : code.getCoding()) { 905 ValidationResult res = verifyCodeInternal(vs, c.getSystem(), c.getCode(), c.getDisplay()); 906 if (res.isOk()) { 907 return res; 908 } 909 } 910 if (code.getCoding().isEmpty()) { 911 return new ValidationResult(IssueSeverity.ERROR, "None code provided"); 912 } else { 913 return new ValidationResult(IssueSeverity.ERROR, 914 "None of the codes are in the specified value set"); 915 } 916 } 917 918 private ValidationResult verifyCodeInternal(ValueSet vs, String system, String code, 919 String display) throws Exception { 920 if (vs.hasExpansion()) { 921 return verifyCodeInExpansion(vs, system, code, display); 922 } else { 923 ValueSetExpansionOutcome vse = expansionCache.getExpander().expand(vs, null); 924 if (vse.getValueset() != null) { 925 return verifyCodeExternal(vs, 926 new Coding().setSystem(system).setCode(code).setDisplay(display), false); 927 } else { 928 return verifyCodeInExpansion(vse.getValueset(), system, code, display); 929 } 930 } 931 } 932 933 private ValidationResult verifyCodeInternal(ValueSet vs, String code) 934 throws FileNotFoundException, ETooCostly, IOException, FHIRException { 935 if (vs.hasExpansion()) { 936 return verifyCodeInExpansion(vs, code); 937 } else { 938 ValueSetExpansionOutcome vse = expansionCache.getExpander().expand(vs, null); 939 if (vse.getValueset() == null) { 940 return new ValidationResult(IssueSeverity.ERROR, vse.getError(), vse.getErrorClass()); 941 } else { 942 return verifyCodeInExpansion(vse.getValueset(), code); 943 } 944 } 945 } 946 947 private ValidationResult verifyCodeInCodeSystem(CodeSystem cs, String system, String code, 948 String display) throws Exception { 949 ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code); 950 if (cc == null) { 951 if (cs.getContent().equals(CodeSystem.CodeSystemContentMode.COMPLETE)) { 952 return new ValidationResult(IssueSeverity.ERROR, 953 "Unknown Code " + code + " in " + cs.getUrl()); 954 } else if (!cs.getContent().equals(CodeSystem.CodeSystemContentMode.NOTPRESENT)) { 955 return new ValidationResult(IssueSeverity.WARNING, 956 "Unknown Code " + code + " in partial code list of " + cs.getUrl()); 957 } else { 958 return verifyCodeExternal(null, 959 new Coding().setSystem(system).setCode(code).setDisplay(display), false); 960 } 961 } 962 // 963 // return new ValidationResult(IssueSeverity.WARNING, "A definition was found for "+cs.getUrl()+", but it has no codes in the definition"); 964 // return new ValidationResult(IssueSeverity.ERROR, "Unknown Code "+code+" in "+cs.getUrl()); 965 if (display == null) { 966 return new ValidationResult(cc); 967 } 968 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 969 if (cc.hasDisplay()) { 970 b.append(cc.getDisplay()); 971 if (display.equalsIgnoreCase(cc.getDisplay())) { 972 return new ValidationResult(cc); 973 } 974 } 975 for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) { 976 b.append(ds.getValue()); 977 if (display.equalsIgnoreCase(ds.getValue())) { 978 return new ValidationResult(cc); 979 } 980 } 981 return new ValidationResult(IssueSeverity.WARNING, 982 "Display Name for " + code + " must be one of '" + b.toString() + "'", cc); 983 } 984 985 986 private ValidationResult verifyCodeInExpansion(ValueSet vs, String system, String code, 987 String display) { 988 ValueSetExpansionContainsComponent cc = findCode(vs.getExpansion().getContains(), code); 989 if (cc == null) { 990 return new ValidationResult(IssueSeverity.ERROR, 991 "Unknown Code " + code + " in " + vs.getUrl()); 992 } 993 if (display == null) { 994 return new ValidationResult( 995 new ConceptDefinitionComponent().setCode(code).setDisplay(cc.getDisplay())); 996 } 997 if (cc.hasDisplay()) { 998 if (display.equalsIgnoreCase(cc.getDisplay())) { 999 return new ValidationResult( 1000 new ConceptDefinitionComponent().setCode(code).setDisplay(cc.getDisplay())); 1001 } 1002 return new ValidationResult(IssueSeverity.WARNING, 1003 "Display Name for " + code + " must be '" + cc.getDisplay() + "'", 1004 new ConceptDefinitionComponent().setCode(code).setDisplay(cc.getDisplay())); 1005 } 1006 return null; 1007 } 1008 1009 private ValidationResult verifyCodeInExpansion(ValueSet vs, String code) throws FHIRException { 1010 if (vs.getExpansion() 1011 .hasExtension("http://hl7.org/fhir/StructureDefinition/valueset-toocostly")) { 1012 throw new FHIRException("Unable to validate core - value set is too costly to expand"); 1013 } else { 1014 ValueSetExpansionContainsComponent cc = findCode(vs.getExpansion().getContains(), code); 1015 if (cc == null) { 1016 return new ValidationResult(IssueSeverity.ERROR, 1017 "Unknown Code " + code + " in " + vs.getUrl()); 1018 } 1019 return null; 1020 } 1021 } 1022 1023 private ValueSetExpansionContainsComponent findCode( 1024 List<ValueSetExpansionContainsComponent> contains, String code) { 1025 for (ValueSetExpansionContainsComponent cc : contains) { 1026 if (code.equals(cc.getCode())) { 1027 return cc; 1028 } 1029 ValueSetExpansionContainsComponent c = findCode(cc.getContains(), code); 1030 if (c != null) { 1031 return c; 1032 } 1033 } 1034 return null; 1035 } 1036 1037 private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, 1038 String code) { 1039 for (ConceptDefinitionComponent cc : concept) { 1040 if (code.equals(cc.getCode())) { 1041 return cc; 1042 } 1043 ConceptDefinitionComponent c = findCodeInConcept(cc.getConcept(), code); 1044 if (c != null) { 1045 return c; 1046 } 1047 } 1048 return null; 1049 } 1050 1051 public Set<String> getNonSupportedCodeSystems() { 1052 return nonSupportedCodeSystems; 1053 } 1054 1055 public boolean isCanRunWithoutTerminology() { 1056 return canRunWithoutTerminology; 1057 } 1058 1059 public void setCanRunWithoutTerminology(boolean canRunWithoutTerminology) { 1060 this.canRunWithoutTerminology = canRunWithoutTerminology; 1061 } 1062 1063 public int getExpandCodesLimit() { 1064 return expandCodesLimit; 1065 } 1066 1067 public void setExpandCodesLimit(int expandCodesLimit) { 1068 this.expandCodesLimit = expandCodesLimit; 1069 } 1070 1071 @Deprecated 1072 public void setLogger(ILoggingService logger) { 1073 1074 } 1075 1076 public ExpansionProfile getExpansionProfile() { 1077 return expProfile; 1078 } 1079 1080 public void setExpansionProfile(ExpansionProfile expProfile) { 1081 this.expProfile = expProfile; 1082 } 1083 1084 @Override 1085 public boolean isNoTerminologyServer() { 1086 return noTerminologyServer; 1087 } 1088 1089 public String getName() { 1090 return name; 1091 } 1092 1093 public void setName(String name) { 1094 this.name = name; 1095 } 1096 1097 @Override 1098 public Set<String> getResourceNamesAsSet() { 1099 Set<String> res = new HashSet<String>(); 1100 res.addAll(getResourceNames()); 1101 return res; 1102 } 1103 1104 public void reportStatus(JsonObject json) { 1105 json.addProperty("codeystem-count", codeSystems.size()); 1106 json.addProperty("valueset-count", valueSets.size()); 1107 json.addProperty("conceptmap-count", maps.size()); 1108 json.addProperty("transforms-count", transforms.size()); 1109 json.addProperty("structures-count", profiles.size()); 1110 } 1111 1112 public void cacheResource(Resource r) throws Exception { 1113 if (r instanceof ValueSet) { 1114 seeValueSet(((ValueSet) r).getUrl(), (ValueSet) r); 1115 } else if (r instanceof CodeSystem) { 1116 seeCodeSystem(((CodeSystem) r).getUrl(), (CodeSystem) r); 1117 } else if (r instanceof StructureDefinition) { 1118 StructureDefinition sd = (StructureDefinition) r; 1119 if ("http://hl7.org/fhir/StructureDefinition/Extension".equals(sd.getBaseDefinition())) { 1120 seeExtensionDefinition(sd.getUrl(), sd); 1121 } else if (sd.getDerivation() == TypeDerivationRule.CONSTRAINT) { 1122 seeProfile(sd.getUrl(), sd); 1123 } 1124 } 1125 } 1126 1127 public void dropResource(String type, String id) throws FHIRException { 1128 if (type.equals("ValueSet")) { 1129 dropValueSet(id); 1130 } 1131 if (type.equals("CodeSystem")) { 1132 dropCodeSystem(id); 1133 } 1134 if (type.equals("StructureDefinition")) { 1135 dropProfile(id); 1136 dropExtensionDefinition(id); 1137 } 1138 } 1139 1140 public boolean isAllowLoadingDuplicates() { 1141 return allowLoadingDuplicates; 1142 } 1143 1144 public void setAllowLoadingDuplicates(boolean allowLoadingDuplicates) { 1145 this.allowLoadingDuplicates = allowLoadingDuplicates; 1146 } 1147 1148 @Override 1149 public StructureDefinition fetchTypeDefinition(String typeName) { 1150 return fetchResource(StructureDefinition.class, 1151 "http://hl7.org/fhir/StructureDefinition/" + typeName); 1152 } 1153}