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