001package org.hl7.fhir.r5.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.File; 035import java.io.FileNotFoundException; 036import java.io.FileOutputStream; 037import java.io.IOException; 038import java.io.OutputStreamWriter; 039import java.util.*; 040 041import lombok.Getter; 042import lombok.Setter; 043import lombok.experimental.Accessors; 044import org.apache.commons.lang3.StringUtils; 045import org.hl7.fhir.exceptions.FHIRException; 046import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult; 047import org.hl7.fhir.r5.formats.IParser.OutputStyle; 048import org.hl7.fhir.r5.formats.JsonParser; 049import org.hl7.fhir.r5.model.*; 050import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; 051import org.hl7.fhir.r5.model.OperationOutcome.OperationOutcomeIssueComponent; 052import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; 053import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent; 054import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; 055import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome; 056import org.hl7.fhir.r5.terminologies.utilities.TerminologyServiceErrorClass; 057import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 058import org.hl7.fhir.utilities.TextFile; 059import org.hl7.fhir.utilities.Utilities; 060import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 061import org.hl7.fhir.utilities.validation.ValidationOptions; 062 063import com.google.gson.JsonElement; 064import com.google.gson.JsonObject; 065import com.google.gson.JsonPrimitive; 066 067/** 068 * This implements a two level cache. 069 * - a temporary cache for remembering previous local operations 070 * - a persistent cache for remembering tx server operations 071 * 072 * the cache is a series of pairs: a map, and a list. the map is the loaded cache, the list is the persistent cache, carefully maintained in order for version control consistency 073 * 074 * @author graha 075 * 076 */ 077public class TerminologyCache { 078 079 public static final boolean TRANSIENT = false; 080 public static final boolean PERMANENT = true; 081 private static final String NAME_FOR_NO_SYSTEM = "all-systems"; 082 private static final String ENTRY_MARKER = "-------------------------------------------------------------------------------------"; 083 private static final String BREAK = "####"; 084 private static final String CACHE_FILE_EXTENSION = ".cache"; 085 private static final String CAPABILITY_STATEMENT_TITLE = ".capabilityStatement"; 086 private static final String TERMINOLOGY_CAPABILITIES_TITLE = ".terminologyCapabilities"; 087 088 089 private SystemNameKeyGenerator systemNameKeyGenerator = new SystemNameKeyGenerator(); 090 091 public class CacheToken { 092 @Getter 093 private String name; 094 private String key; 095 @Getter 096 private String request; 097 @Accessors(fluent = true) 098 @Getter 099 private boolean hasVersion; 100 101 public void setName(String n) { 102 String systemName = getSystemNameKeyGenerator().getNameForSystem(n); 103 if (name == null) 104 name = systemName; 105 else if (!systemName.equals(name)) 106 name = NAME_FOR_NO_SYSTEM; 107 } 108 } 109 110 protected SystemNameKeyGenerator getSystemNameKeyGenerator() { 111 return systemNameKeyGenerator; 112 } 113 public class SystemNameKeyGenerator { 114 public static final String SNOMED_SCT_CODESYSTEM_URL = "http://snomed.info/sct"; 115 public static final String RXNORM_CODESYSTEM_URL = "http://www.nlm.nih.gov/research/umls/rxnorm"; 116 public static final String LOINC_CODESYSTEM_URL = "http://loinc.org"; 117 public static final String UCUM_CODESYSTEM_URL = "http://unitsofmeasure.org"; 118 119 public static final String HL7_TERMINOLOGY_CODESYSTEM_BASE_URL = "http://terminology.hl7.org/CodeSystem/"; 120 public static final String HL7_SID_CODESYSTEM_BASE_URL = "http://hl7.org/fhir/sid/"; 121 public static final String HL7_FHIR_CODESYSTEM_BASE_URL = "http://hl7.org/fhir/"; 122 123 public static final String ISO_CODESYSTEM_URN = "urn:iso:std:iso:"; 124 public static final String LANG_CODESYSTEM_URN = "urn:ietf:bcp:47"; 125 public static final String MIMETYPES_CODESYSTEM_URN = "urn:ietf:bcp:13"; 126 127 public static final String _11073_CODESYSTEM_URN = "urn:iso:std:iso:11073:10101"; 128 public static final String DICOM_CODESYSTEM_URL = "http://dicom.nema.org/resources/ontology/DCM"; 129 130 public String getNameForSystem(String system) { 131 final int lastPipe = system.lastIndexOf('|'); 132 final String systemBaseName = lastPipe == -1 ? system : system.substring(0,lastPipe); 133 final String systemVersion = lastPipe == -1 ? null : system.substring(lastPipe + 1); 134 135 if (systemBaseName.equals(SNOMED_SCT_CODESYSTEM_URL)) 136 return getVersionedSystem("snomed", systemVersion); 137 if (systemBaseName.equals(RXNORM_CODESYSTEM_URL)) 138 return getVersionedSystem("rxnorm", systemVersion); 139 if (systemBaseName.equals(LOINC_CODESYSTEM_URL)) 140 return getVersionedSystem("loinc", systemVersion); 141 if (systemBaseName.equals(UCUM_CODESYSTEM_URL)) 142 return getVersionedSystem("ucum", systemVersion); 143 if (systemBaseName.startsWith(HL7_SID_CODESYSTEM_BASE_URL)) 144 return getVersionedSystem(normalizeBaseURL(HL7_SID_CODESYSTEM_BASE_URL, systemBaseName), systemVersion); 145 if (systemBaseName.equals(_11073_CODESYSTEM_URN)) 146 return getVersionedSystem("11073", systemVersion); 147 if (systemBaseName.startsWith(ISO_CODESYSTEM_URN)) 148 return getVersionedSystem("iso"+systemBaseName.substring(ISO_CODESYSTEM_URN.length()).replace(":", ""), systemVersion); 149 if (systemBaseName.startsWith(HL7_TERMINOLOGY_CODESYSTEM_BASE_URL)) 150 return getVersionedSystem(normalizeBaseURL(HL7_TERMINOLOGY_CODESYSTEM_BASE_URL, systemBaseName), systemVersion); 151 if (systemBaseName.startsWith(HL7_FHIR_CODESYSTEM_BASE_URL)) 152 return getVersionedSystem(normalizeBaseURL(HL7_FHIR_CODESYSTEM_BASE_URL, systemBaseName), systemVersion); 153 if (systemBaseName.equals(LANG_CODESYSTEM_URN)) 154 return getVersionedSystem("lang", systemVersion); 155 if (systemBaseName.equals(MIMETYPES_CODESYSTEM_URN)) 156 return getVersionedSystem("mimetypes", systemVersion); 157 if (systemBaseName.equals(DICOM_CODESYSTEM_URL)) 158 return getVersionedSystem("dicom", systemVersion); 159 return getVersionedSystem(systemBaseName.replace("/", "_").replace(":", "_").replace("?", "X").replace("#", "X"), systemVersion); 160 } 161 162 public String normalizeBaseURL(String baseUrl, String fullUrl) { 163 return fullUrl.substring(baseUrl.length()).replace("/", ""); 164 } 165 166 public String getVersionedSystem(String baseSystem, String version) { 167 if (version != null) { 168 return baseSystem + "_" + version; 169 } 170 return baseSystem; 171 } 172 } 173 174 175 private class CacheEntry { 176 private String request; 177 private boolean persistent; 178 private ValidationResult v; 179 private ValueSetExpansionOutcome e; 180 } 181 182 private class NamedCache { 183 private String name; 184 private List<CacheEntry> list = new ArrayList<CacheEntry>(); // persistent entries 185 private Map<String, CacheEntry> map = new HashMap<String, CacheEntry>(); 186 } 187 188 189 private Object lock; 190 private String folder; 191 @Getter private int requestCount; 192 @Getter private int hitCount; 193 @Getter private int networkCount; 194 private CapabilityStatement capabilityStatementCache = null; 195 private TerminologyCapabilities terminologyCapabilitiesCache = null; 196 private Map<String, NamedCache> caches = new HashMap<String, NamedCache>(); 197 @Getter @Setter private static boolean noCaching; 198 199 @Getter @Setter private static boolean cacheErrors; 200 201 202 // use lock from the context 203 public TerminologyCache(Object lock, String folder) throws FileNotFoundException, IOException, FHIRException { 204 super(); 205 this.lock = lock; 206 this.folder = folder; 207 requestCount = 0; 208 hitCount = 0; 209 networkCount = 0; 210 211 if (folder != null) { 212 load(); 213 } 214 } 215 216 public boolean hasCapabilityStatement() { 217 return capabilityStatementCache != null; 218 } 219 220 public CapabilityStatement getCapabilityStatement() { 221 return capabilityStatementCache; 222 } 223 224 public void cacheCapabilityStatement(CapabilityStatement capabilityStatement) { 225 if (noCaching) { 226 return; 227 } 228 this.capabilityStatementCache = capabilityStatement; 229 save(capabilityStatementCache, CAPABILITY_STATEMENT_TITLE); 230 } 231 232 233 public boolean hasTerminologyCapabilities() { 234 return terminologyCapabilitiesCache != null; 235 } 236 237 public TerminologyCapabilities getTerminologyCapabilities() { 238 return terminologyCapabilitiesCache; 239 } 240 241 public void cacheTerminologyCapabilities(TerminologyCapabilities terminologyCapabilities) { 242 if (noCaching) { 243 return; 244 } 245 this.terminologyCapabilitiesCache = terminologyCapabilities; 246 save(terminologyCapabilitiesCache, TERMINOLOGY_CAPABILITIES_TITLE); 247 } 248 249 250 public void clear() { 251 caches.clear(); 252 } 253 254 public CacheToken generateValidationToken(ValidationOptions options, Coding code, ValueSet vs, Parameters expParameters) { 255 try { 256 CacheToken ct = new CacheToken(); 257 if (code.hasSystem()) { 258 ct.setName(code.getSystem()); 259 ct.hasVersion = code.hasVersion(); 260 } 261 else 262 ct.name = NAME_FOR_NO_SYSTEM; 263 nameCacheToken(vs, ct); 264 JsonParser json = new JsonParser(); 265 json.setOutputStyle(OutputStyle.PRETTY); 266 String expJS = json.composeString(expParameters); 267 268 if (vs != null && vs.hasUrl() && vs.hasVersion()) { 269 ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+", \"url\": \""+Utilities.escapeJson(vs.getUrl()) 270 +"\", \"version\": \""+Utilities.escapeJson(vs.getVersion())+"\""+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}\r\n"; 271 } else { 272 ValueSet vsc = getVSEssense(vs); 273 ct.request = "{\"code\" : "+json.composeString(code, "code")+", \"valueSet\" :"+(vsc == null ? "null" : extracted(json, vsc))+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}"; 274 } 275 ct.key = String.valueOf(hashJson(ct.request)); 276 return ct; 277 } catch (IOException e) { 278 throw new Error(e); 279 } 280 } 281 282 public CacheToken generateValidationToken(ValidationOptions options, Coding code, String vsUrl, Parameters expParameters) { 283 try { 284 CacheToken ct = new CacheToken(); 285 if (code.hasSystem()) { 286 ct.setName(code.getSystem()); 287 ct.hasVersion = code.hasVersion(); 288 } 289 else 290 ct.name = NAME_FOR_NO_SYSTEM; 291 ct.setName(vsUrl); 292 JsonParser json = new JsonParser(); 293 json.setOutputStyle(OutputStyle.PRETTY); 294 String expJS = json.composeString(expParameters); 295 296 ct.request = "{\"code\" : "+json.composeString(code, "code")+", \"valueSet\" :"+(vsUrl == null ? "null" : vsUrl)+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}"; 297 ct.key = String.valueOf(hashJson(ct.request)); 298 return ct; 299 } catch (IOException e) { 300 throw new Error(e); 301 } 302 } 303 304 public String extracted(JsonParser json, ValueSet vsc) throws IOException { 305 String s = null; 306 if (vsc.getExpansion().getContains().size() > 1000 || vsc.getCompose().getIncludeFirstRep().getConcept().size() > 1000) { 307 s = vsc.getUrl(); 308 } else { 309 s = json.composeString(vsc); 310 } 311 return s; 312 } 313 314 public CacheToken generateValidationToken(ValidationOptions options, CodeableConcept code, ValueSet vs, Parameters expParameters) { 315 try { 316 CacheToken ct = new CacheToken(); 317 for (Coding c : code.getCoding()) { 318 if (c.hasSystem()) { 319 ct.setName(c.getSystem()); 320 ct.hasVersion = c.hasVersion(); 321 } 322 } 323 nameCacheToken(vs, ct); 324 JsonParser json = new JsonParser(); 325 json.setOutputStyle(OutputStyle.PRETTY); 326 String expJS = json.composeString(expParameters); 327 if (vs != null && vs.hasUrl() && vs.hasVersion()) { 328 ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+", \"url\": \""+Utilities.escapeJson(vs.getUrl())+ 329 "\", \"version\": \""+Utilities.escapeJson(vs.getVersion())+"\""+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}\r\n"; 330 } else { 331 ValueSet vsc = getVSEssense(vs); 332 ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+", \"valueSet\" :"+extracted(json, vsc)+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}"; 333 } 334 ct.key = String.valueOf(hashJson(ct.request)); 335 return ct; 336 } catch (IOException e) { 337 throw new Error(e); 338 } 339 } 340 341 public ValueSet getVSEssense(ValueSet vs) { 342 if (vs == null) 343 return null; 344 ValueSet vsc = new ValueSet(); 345 vsc.setCompose(vs.getCompose()); 346 if (vs.hasExpansion()) { 347 vsc.getExpansion().getParameter().addAll(vs.getExpansion().getParameter()); 348 vsc.getExpansion().getContains().addAll(vs.getExpansion().getContains()); 349 } 350 return vsc; 351 } 352 353 public CacheToken generateExpandToken(ValueSet vs, boolean hierarchical) { 354 CacheToken ct = new CacheToken(); 355 nameCacheToken(vs, ct); 356 if (vs.hasUrl() && vs.hasVersion()) { 357 ct.request = "{\"hierarchical\" : "+(hierarchical ? "true" : "false")+", \"url\": \""+Utilities.escapeJson(vs.getUrl())+"\", \"version\": \""+Utilities.escapeJson(vs.getVersion())+"\"}\r\n"; 358 } else { 359 ValueSet vsc = getVSEssense(vs); 360 JsonParser json = new JsonParser(); 361 json.setOutputStyle(OutputStyle.PRETTY); 362 try { 363 ct.request = "{\"hierarchical\" : "+(hierarchical ? "true" : "false")+", \"valueSet\" :"+extracted(json, vsc)+"}\r\n"; 364 } catch (IOException e) { 365 throw new Error(e); 366 } 367 } 368 ct.key = String.valueOf(hashJson(ct.request)); 369 return ct; 370 } 371 372 public void nameCacheToken(ValueSet vs, CacheToken ct) { 373 if (vs != null) { 374 for (ConceptSetComponent inc : vs.getCompose().getInclude()) { 375 if (inc.hasSystem()) { 376 ct.setName(inc.getSystem()); 377 ct.hasVersion = inc.hasVersion(); 378 } 379 } 380 for (ConceptSetComponent inc : vs.getCompose().getExclude()) { 381 if (inc.hasSystem()) { 382 ct.setName(inc.getSystem()); 383 ct.hasVersion = inc.hasVersion(); 384 } 385 } 386 for (ValueSetExpansionContainsComponent inc : vs.getExpansion().getContains()) { 387 if (inc.hasSystem()) { 388 ct.setName(inc.getSystem()); 389 ct.hasVersion = inc.hasVersion(); 390 } 391 } 392 } 393 } 394 395 private String normalizeSystemPath(String path) { 396 return path.replace("/", "").replace('|','X'); 397 } 398 399 400 401 public NamedCache getNamedCache(CacheToken cacheToken) { 402 403 final String cacheName = cacheToken.name == null ? "null" : cacheToken.name; 404 405 NamedCache nc = caches.get(cacheName); 406 407 if (nc == null) { 408 nc = new NamedCache(); 409 nc.name = cacheName; 410 caches.put(nc.name, nc); 411 } 412 return nc; 413 } 414 415 public ValueSetExpansionOutcome getExpansion(CacheToken cacheToken) { 416 synchronized (lock) { 417 NamedCache nc = getNamedCache(cacheToken); 418 CacheEntry e = nc.map.get(cacheToken.key); 419 if (e == null) 420 return null; 421 else 422 return e.e; 423 } 424 } 425 426 public void cacheExpansion(CacheToken cacheToken, ValueSetExpansionOutcome res, boolean persistent) { 427 synchronized (lock) { 428 NamedCache nc = getNamedCache(cacheToken); 429 CacheEntry e = new CacheEntry(); 430 e.request = cacheToken.request; 431 e.persistent = persistent; 432 e.e = res; 433 store(cacheToken, persistent, nc, e); 434 } 435 } 436 437 public void store(CacheToken cacheToken, boolean persistent, NamedCache nc, CacheEntry e) { 438 if (noCaching) { 439 return; 440 } 441 442 if ( !cacheErrors && 443 ( e.v!= null 444 && e.v.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED 445 && !cacheToken.hasVersion)) { 446 return; 447 } 448 449 boolean n = nc.map.containsKey(cacheToken.key); 450 nc.map.put(cacheToken.key, e); 451 if (persistent) { 452 if (n) { 453 for (int i = nc.list.size()- 1; i>= 0; i--) { 454 if (nc.list.get(i).request.equals(e.request)) { 455 nc.list.remove(i); 456 } 457 } 458 } 459 nc.list.add(e); 460 save(nc); 461 } 462 } 463 464 public ValidationResult getValidation(CacheToken cacheToken) { 465 if (cacheToken.key == null) { 466 return null; 467 } 468 synchronized (lock) { 469 requestCount++; 470 NamedCache nc = getNamedCache(cacheToken); 471 CacheEntry e = nc.map.get(cacheToken.key); 472 if (e == null) { 473 networkCount++; 474 return null; 475 } else { 476 hitCount++; 477 return e.v; 478 } 479 } 480 } 481 482 public void cacheValidation(CacheToken cacheToken, ValidationResult res, boolean persistent) { 483 if (cacheToken.key != null) { 484 synchronized (lock) { 485 NamedCache nc = getNamedCache(cacheToken); 486 CacheEntry e = new CacheEntry(); 487 e.request = cacheToken.request; 488 e.persistent = persistent; 489 e.v = res; 490 store(cacheToken, persistent, nc, e); 491 } 492 } 493 } 494 495 496 // persistence 497 498 public void save() { 499 500 } 501 502 private <K extends Resource> void save(K resource, String title) { 503 if (folder == null) 504 return; 505 506 try { 507 OutputStreamWriter sw = new OutputStreamWriter(new FileOutputStream(Utilities.path(folder, title + CACHE_FILE_EXTENSION)), "UTF-8"); 508 509 JsonParser json = new JsonParser(); 510 json.setOutputStyle(OutputStyle.PRETTY); 511 512 sw.write(json.composeString(resource).trim()); 513 sw.close(); 514 } catch (Exception e) { 515 System.out.println("error saving capability statement "+e.getMessage()); 516 } 517 } 518 519 private void save(NamedCache nc) { 520 if (folder == null) 521 return; 522 523 try { 524 OutputStreamWriter sw = new OutputStreamWriter(new FileOutputStream(Utilities.path(folder, nc.name+CACHE_FILE_EXTENSION)), "UTF-8"); 525 sw.write(ENTRY_MARKER+"\r\n"); 526 JsonParser json = new JsonParser(); 527 json.setOutputStyle(OutputStyle.PRETTY); 528 for (CacheEntry ce : nc.list) { 529 sw.write(ce.request.trim()); 530 sw.write(BREAK+"\r\n"); 531 if (ce.e != null) { 532 sw.write("e: {\r\n"); 533 if (ce.e.getValueset() != null) 534 sw.write(" \"valueSet\" : "+json.composeString(ce.e.getValueset()).trim()+",\r\n"); 535 sw.write(" \"error\" : \""+Utilities.escapeJson(ce.e.getError()).trim()+"\"\r\n}\r\n"); 536 } else { 537 sw.write("v: {\r\n"); 538 boolean first = true; 539 if (ce.v.getDisplay() != null) { 540 if (first) first = false; else sw.write(",\r\n"); 541 sw.write(" \"display\" : \""+Utilities.escapeJson(ce.v.getDisplay()).trim()+"\""); 542 } 543 if (ce.v.getCode() != null) { 544 if (first) first = false; else sw.write(",\r\n"); 545 sw.write(" \"code\" : \""+Utilities.escapeJson(ce.v.getCode()).trim()+"\""); 546 } 547 if (ce.v.getSystem() != null) { 548 if (first) first = false; else sw.write(",\r\n"); 549 sw.write(" \"system\" : \""+Utilities.escapeJson(ce.v.getSystem()).trim()+"\""); 550 } 551 if (ce.v.getVersion() != null) { 552 if (first) first = false; else sw.write(",\r\n"); 553 sw.write(" \"version\" : \""+Utilities.escapeJson(ce.v.getVersion()).trim()+"\""); 554 } 555 if (ce.v.getSeverity() != null) { 556 if (first) first = false; else sw.write(",\r\n"); 557 sw.write(" \"severity\" : "+"\""+ce.v.getSeverity().toCode().trim()+"\""+""); 558 } 559 if (ce.v.getMessage() != null) { 560 if (first) first = false; else sw.write(",\r\n"); 561 sw.write(" \"error\" : \""+Utilities.escapeJson(ce.v.getMessage()).trim()+"\""); 562 } 563 if (ce.v.getErrorClass() != null) { 564 if (first) first = false; else sw.write(",\r\n"); 565 sw.write(" \"class\" : \""+Utilities.escapeJson(ce.v.getErrorClass().toString())+"\""); 566 } 567 if (ce.v.getDefinition() != null) { 568 if (first) first = false; else sw.write(",\r\n"); 569 sw.write(" \"definition\" : \""+Utilities.escapeJson(ce.v.getDefinition()).trim()+"\""); 570 } 571 if (ce.v.getUnknownSystems() != null) { 572 if (first) first = false; else sw.write(",\r\n"); 573 sw.write(" \"unknown-systems\" : \""+Utilities.escapeJson(CommaSeparatedStringBuilder.join(",", ce.v.getUnknownSystems())).trim()+"\""); 574 } 575 if (ce.v.getIssues() != null) { 576 if (first) first = false; else sw.write(",\r\n"); 577 OperationOutcome oo = new OperationOutcome(); 578 oo.setIssue(ce.v.getIssues()); 579 sw.write(" \"issues\" : "+json.composeString(oo).trim()+"\r\n"); 580 } 581 sw.write("\r\n}\r\n"); 582 } 583 sw.write(ENTRY_MARKER+"\r\n"); 584 } 585 sw.close(); 586 } catch (Exception e) { 587 System.out.println("error saving "+nc.name+": "+e.getMessage()); 588 } 589 } 590 591 private boolean isCapabilityCache(String fn) { 592 if (fn == null) { 593 return false; 594 } 595 return fn.startsWith(CAPABILITY_STATEMENT_TITLE) || fn.startsWith(TERMINOLOGY_CAPABILITIES_TITLE); 596 } 597 598 private void loadCapabilityCache(String fn) { 599 try { 600 String src = TextFile.fileToString(Utilities.path(folder, fn)); 601 602 JsonObject o = (JsonObject) new com.google.gson.JsonParser().parse(src); 603 Resource resource = new JsonParser().parse(o); 604 605 if (fn.startsWith(CAPABILITY_STATEMENT_TITLE)) { 606 this.capabilityStatementCache = (CapabilityStatement) resource; 607 } else if (fn.startsWith(TERMINOLOGY_CAPABILITIES_TITLE)) { 608 this.terminologyCapabilitiesCache = (TerminologyCapabilities) resource; 609 } 610 } catch (Exception e) { 611 e.printStackTrace(); 612 throw new FHIRException("Error loading " + fn + ": " + e.getMessage(), e); 613 } 614 } 615 616 private CacheEntry getCacheEntry(String request, String resultString) throws IOException { 617 CacheEntry ce = new CacheEntry(); 618 ce.persistent = true; 619 ce.request = request; 620 boolean e = resultString.charAt(0) == 'e'; 621 resultString = resultString.substring(3); 622 JsonObject o = (JsonObject) new com.google.gson.JsonParser().parse(resultString); 623 String error = loadJS(o.get("error")); 624 if (e) { 625 if (o.has("valueSet")) 626 ce.e = new ValueSetExpansionOutcome((ValueSet) new JsonParser().parse(o.getAsJsonObject("valueSet")), error, TerminologyServiceErrorClass.UNKNOWN); 627 else 628 ce.e = new ValueSetExpansionOutcome(error, TerminologyServiceErrorClass.UNKNOWN); 629 } else { 630 String t = loadJS(o.get("severity")); 631 IssueSeverity severity = t == null ? null : IssueSeverity.fromCode(t); 632 String display = loadJS(o.get("display")); 633 String code = loadJS(o.get("code")); 634 String system = loadJS(o.get("system")); 635 String version = loadJS(o.get("version")); 636 String definition = loadJS(o.get("definition")); 637 String unknownSystems = loadJS(o.get("unknown-systems")); 638 OperationOutcome oo = o.has("issues") ? (OperationOutcome) new JsonParser().parse(o.getAsJsonObject("issues")) : null; 639 t = loadJS(o.get("class")); 640 TerminologyServiceErrorClass errorClass = t == null ? null : TerminologyServiceErrorClass.valueOf(t) ; 641 ce.v = new ValidationResult(severity, error, system, version, new ConceptDefinitionComponent().setDisplay(display).setDefinition(definition).setCode(code), display, null).setErrorClass(errorClass); 642 ce.v.setUnknownSystems(CommaSeparatedStringBuilder.toSet(unknownSystems)); 643 if (oo != null) { 644 ce.v.setIssues(oo.getIssue()); 645 } 646 } 647 return ce; 648 } 649 650 private void loadNamedCache(String fn) { 651 int c = 0; 652 try { 653 String src = TextFile.fileToString(Utilities.path(folder, fn)); 654 String title = fn.substring(0, fn.lastIndexOf(".")); 655 656 NamedCache nc = new NamedCache(); 657 nc.name = title; 658 659 if (src.startsWith("?")) 660 src = src.substring(1); 661 int i = src.indexOf(ENTRY_MARKER); 662 while (i > -1) { 663 c++; 664 String s = src.substring(0, i); 665 src = src.substring(i + ENTRY_MARKER.length() + 1); 666 i = src.indexOf(ENTRY_MARKER); 667 if (!Utilities.noString(s)) { 668 int j = s.indexOf(BREAK); 669 String request = s.substring(0, j); 670 String p = s.substring(j + BREAK.length() + 1).trim(); 671 672 CacheEntry cacheEntry = getCacheEntry(request, p); 673 674 nc.map.put(String.valueOf(hashJson(cacheEntry.request)), cacheEntry); 675 nc.list.add(cacheEntry); 676 } 677 caches.put(nc.name, nc); 678 } 679 } catch (Exception e) { 680 System.out.println("Error loading "+fn+": "+e.getMessage()+" entry "+c+" - ignoring it"); 681 e.printStackTrace(); 682 } 683 } 684 685 private void load() throws FHIRException { 686 for (String fn : new File(folder).list()) { 687 if (fn.endsWith(CACHE_FILE_EXTENSION) && !fn.equals("validation" + CACHE_FILE_EXTENSION)) { 688 try { 689 if (isCapabilityCache(fn)) { 690 loadCapabilityCache(fn); 691 } else { 692 loadNamedCache(fn); 693 } 694 } catch (FHIRException e) { 695 throw e; 696 } 697 } 698 } 699 } 700 701 private String loadJS(JsonElement e) { 702 if (e == null) 703 return null; 704 if (!(e instanceof JsonPrimitive)) 705 return null; 706 String s = e.getAsString(); 707 if ("".equals(s)) 708 return null; 709 return s; 710 } 711 712 protected String hashJson(String s) { 713 return String.valueOf(s.trim().hashCode()); 714 } 715 716 // management 717 718 public String summary(ValueSet vs) { 719 if (vs == null) 720 return "null"; 721 722 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 723 for (ConceptSetComponent cc : vs.getCompose().getInclude()) 724 b.append("Include "+getIncSummary(cc)); 725 for (ConceptSetComponent cc : vs.getCompose().getExclude()) 726 b.append("Exclude "+getIncSummary(cc)); 727 return b.toString(); 728 } 729 730 private String getIncSummary(ConceptSetComponent cc) { 731 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 732 for (UriType vs : cc.getValueSet()) 733 b.append(vs.asStringValue()); 734 String vsd = b.length() > 0 ? " where the codes are in the value sets ("+b.toString()+")" : ""; 735 String system = cc.getSystem(); 736 if (cc.hasConcept()) 737 return Integer.toString(cc.getConcept().size())+" codes from "+system+vsd; 738 if (cc.hasFilter()) { 739 String s = ""; 740 for (ConceptSetFilterComponent f : cc.getFilter()) { 741 if (!Utilities.noString(s)) 742 s = s + " & "; 743 s = s + f.getProperty()+" "+(f.hasOp() ? f.getOp().toCode() : "?")+" "+f.getValue(); 744 } 745 return "from "+system+" where "+s+vsd; 746 } 747 return "All codes from "+system+vsd; 748 } 749 750 public String summary(Coding code) { 751 return code.getSystem()+"#"+code.getCode()+": \""+code.getDisplay()+"\""; 752 } 753 754 public String summary(CodeableConcept code) { 755 StringBuilder b = new StringBuilder(); 756 b.append("{"); 757 boolean first = true; 758 for (Coding c : code.getCoding()) { 759 if (first) first = false; else b.append(","); 760 b.append(summary(c)); 761 } 762 b.append("}: \""); 763 b.append(code.getText()); 764 b.append("\""); 765 return b.toString(); 766 } 767 768 public void removeCS(String url) { 769 synchronized (lock) { 770 String name = getSystemNameKeyGenerator().getNameForSystem(url); 771 if (caches.containsKey(name)) { 772 caches.remove(name); 773 } 774 } 775 } 776 777 public String getFolder() { 778 return folder; 779 } 780 781 782}