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