001package org.hl7.fhir.r5.terminologies.utilities; 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.FileInputStream; 036import java.io.FileNotFoundException; 037import java.io.FileOutputStream; 038import java.io.IOException; 039import java.io.OutputStreamWriter; 040import java.util.*; 041 042import lombok.Getter; 043import lombok.Setter; 044import lombok.experimental.Accessors; 045import org.apache.commons.lang3.StringUtils; 046import org.hl7.fhir.exceptions.FHIRException; 047import org.hl7.fhir.exceptions.FHIRFormatError; 048import org.hl7.fhir.r5.formats.IParser.OutputStyle; 049import org.hl7.fhir.r5.context.ILoggingService.LogCategory; 050import org.hl7.fhir.r5.formats.JsonParser; 051import org.hl7.fhir.r5.model.*; 052import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; 053import org.hl7.fhir.r5.model.OperationOutcome.OperationOutcomeIssueComponent; 054import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; 055import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent; 056import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; 057import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome; 058import org.hl7.fhir.r5.terminologies.utilities.TerminologyCache.SourcedValueSet; 059import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 060import org.hl7.fhir.utilities.IniFile; 061import org.hl7.fhir.utilities.StringPair; 062import org.hl7.fhir.utilities.TextFile; 063import org.hl7.fhir.utilities.Utilities; 064import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; 065import org.hl7.fhir.utilities.json.model.JsonNull; 066import org.hl7.fhir.utilities.json.model.JsonProperty; 067import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 068import org.hl7.fhir.utilities.validation.ValidationOptions; 069 070import com.google.gson.JsonElement; 071import com.google.gson.JsonObject; 072import com.google.gson.JsonPrimitive; 073 074/** 075 * This implements a two level cache. 076 * - a temporary cache for remembering previous local operations 077 * - a persistent cache for remembering tx server operations 078 * 079 * 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 080 * 081 * @author graha 082 * 083 */ 084public class TerminologyCache { 085 086 public static class SourcedCodeSystem { 087 private String server; 088 private CodeSystem cs; 089 090 public SourcedCodeSystem(String server, CodeSystem cs) { 091 super(); 092 this.server = server; 093 this.cs = cs; 094 } 095 public String getServer() { 096 return server; 097 } 098 public CodeSystem getCs() { 099 return cs; 100 } 101 } 102 103 104 public static class SourcedCodeSystemEntry { 105 private String server; 106 private String filename; 107 108 public SourcedCodeSystemEntry(String server, String filename) { 109 super(); 110 this.server = server; 111 this.filename = filename; 112 } 113 public String getServer() { 114 return server; 115 } 116 public String getFilename() { 117 return filename; 118 } 119 } 120 121 122 public static class SourcedValueSet { 123 private String server; 124 private ValueSet vs; 125 126 public SourcedValueSet(String server, ValueSet vs) { 127 super(); 128 this.server = server; 129 this.vs = vs; 130 } 131 public String getServer() { 132 return server; 133 } 134 public ValueSet getVs() { 135 return vs; 136 } 137 } 138 139 public static class SourcedValueSetEntry { 140 private String server; 141 private String filename; 142 143 public SourcedValueSetEntry(String server, String filename) { 144 super(); 145 this.server = server; 146 this.filename = filename; 147 } 148 public String getServer() { 149 return server; 150 } 151 public String getFilename() { 152 return filename; 153 } 154 } 155 156 public static final boolean TRANSIENT = false; 157 public static final boolean PERMANENT = true; 158 private static final String NAME_FOR_NO_SYSTEM = "all-systems"; 159 private static final String ENTRY_MARKER = "-------------------------------------------------------------------------------------"; 160 private static final String BREAK = "####"; 161 private static final String CACHE_FILE_EXTENSION = ".cache"; 162 private static final String CAPABILITY_STATEMENT_TITLE = ".capabilityStatement"; 163 private static final String TERMINOLOGY_CAPABILITIES_TITLE = ".terminologyCapabilities"; 164 private static final String FIXED_CACHE_VERSION = "4"; // last change: change the way tx.fhir.org handles expansions 165 166 167 private SystemNameKeyGenerator systemNameKeyGenerator = new SystemNameKeyGenerator(); 168 169 public class CacheToken { 170 @Getter 171 private String name; 172 private String key; 173 @Getter 174 private String request; 175 @Accessors(fluent = true) 176 @Getter 177 private boolean hasVersion; 178 179 public void setName(String n) { 180 String systemName = getSystemNameKeyGenerator().getNameForSystem(n); 181 if (name == null) 182 name = systemName; 183 else if (!systemName.equals(name)) 184 name = NAME_FOR_NO_SYSTEM; 185 } 186 } 187 188 public static class SubsumesResult { 189 190 private Boolean result; 191 192 protected SubsumesResult(Boolean result) { 193 super(); 194 this.result = result; 195 } 196 197 public Boolean getResult() { 198 return result; 199 } 200 201 } 202 203 protected SystemNameKeyGenerator getSystemNameKeyGenerator() { 204 return systemNameKeyGenerator; 205 } 206 public class SystemNameKeyGenerator { 207 public static final String SNOMED_SCT_CODESYSTEM_URL = "http://snomed.info/sct"; 208 public static final String RXNORM_CODESYSTEM_URL = "http://www.nlm.nih.gov/research/umls/rxnorm"; 209 public static final String LOINC_CODESYSTEM_URL = "http://loinc.org"; 210 public static final String UCUM_CODESYSTEM_URL = "http://unitsofmeasure.org"; 211 212 public static final String HL7_TERMINOLOGY_CODESYSTEM_BASE_URL = "http://terminology.hl7.org/CodeSystem/"; 213 public static final String HL7_SID_CODESYSTEM_BASE_URL = "http://hl7.org/fhir/sid/"; 214 public static final String HL7_FHIR_CODESYSTEM_BASE_URL = "http://hl7.org/fhir/"; 215 216 public static final String ISO_CODESYSTEM_URN = "urn:iso:std:iso:"; 217 public static final String LANG_CODESYSTEM_URN = "urn:ietf:bcp:47"; 218 public static final String MIMETYPES_CODESYSTEM_URN = "urn:ietf:bcp:13"; 219 220 public static final String _11073_CODESYSTEM_URN = "urn:iso:std:iso:11073:10101"; 221 public static final String DICOM_CODESYSTEM_URL = "http://dicom.nema.org/resources/ontology/DCM"; 222 223 public String getNameForSystem(String system) { 224 final int lastPipe = system.lastIndexOf('|'); 225 final String systemBaseName = lastPipe == -1 ? system : system.substring(0,lastPipe); 226 final String systemVersion = lastPipe == -1 ? null : system.substring(lastPipe + 1); 227 228 if (systemBaseName.equals(SNOMED_SCT_CODESYSTEM_URL)) 229 return getVersionedSystem("snomed", systemVersion); 230 if (systemBaseName.equals(RXNORM_CODESYSTEM_URL)) 231 return getVersionedSystem("rxnorm", systemVersion); 232 if (systemBaseName.equals(LOINC_CODESYSTEM_URL)) 233 return getVersionedSystem("loinc", systemVersion); 234 if (systemBaseName.equals(UCUM_CODESYSTEM_URL)) 235 return getVersionedSystem("ucum", systemVersion); 236 if (systemBaseName.startsWith(HL7_SID_CODESYSTEM_BASE_URL)) 237 return getVersionedSystem(normalizeBaseURL(HL7_SID_CODESYSTEM_BASE_URL, systemBaseName), systemVersion); 238 if (systemBaseName.equals(_11073_CODESYSTEM_URN)) 239 return getVersionedSystem("11073", systemVersion); 240 if (systemBaseName.startsWith(ISO_CODESYSTEM_URN)) 241 return getVersionedSystem("iso"+systemBaseName.substring(ISO_CODESYSTEM_URN.length()).replace(":", ""), systemVersion); 242 if (systemBaseName.startsWith(HL7_TERMINOLOGY_CODESYSTEM_BASE_URL)) 243 return getVersionedSystem(normalizeBaseURL(HL7_TERMINOLOGY_CODESYSTEM_BASE_URL, systemBaseName), systemVersion); 244 if (systemBaseName.startsWith(HL7_FHIR_CODESYSTEM_BASE_URL)) 245 return getVersionedSystem(normalizeBaseURL(HL7_FHIR_CODESYSTEM_BASE_URL, systemBaseName), systemVersion); 246 if (systemBaseName.equals(LANG_CODESYSTEM_URN)) 247 return getVersionedSystem("lang", systemVersion); 248 if (systemBaseName.equals(MIMETYPES_CODESYSTEM_URN)) 249 return getVersionedSystem("mimetypes", systemVersion); 250 if (systemBaseName.equals(DICOM_CODESYSTEM_URL)) 251 return getVersionedSystem("dicom", systemVersion); 252 return getVersionedSystem(systemBaseName.replace("/", "_").replace(":", "_").replace("?", "X").replace("#", "X"), systemVersion); 253 } 254 255 public String normalizeBaseURL(String baseUrl, String fullUrl) { 256 return fullUrl.substring(baseUrl.length()).replace("/", ""); 257 } 258 259 public String getVersionedSystem(String baseSystem, String version) { 260 if (version != null) { 261 return baseSystem + "_" + version; 262 } 263 return baseSystem; 264 } 265 } 266 267 268 private class CacheEntry { 269 private String request; 270 private boolean persistent; 271 private ValidationResult v; 272 private ValueSetExpansionOutcome e; 273 private SubsumesResult s; 274 } 275 276 private class NamedCache { 277 private String name; 278 private List<CacheEntry> list = new ArrayList<CacheEntry>(); // persistent entries 279 private Map<String, CacheEntry> map = new HashMap<String, CacheEntry>(); 280 } 281 282 283 private Object lock; 284 private String folder; 285 @Getter private int requestCount; 286 @Getter private int hitCount; 287 @Getter private int networkCount; 288 private Map<String, CapabilityStatement> capabilityStatementCache = new HashMap<>(); 289 private Map<String, TerminologyCapabilities> terminologyCapabilitiesCache = new HashMap<>(); 290 private Map<String, NamedCache> caches = new HashMap<String, NamedCache>(); 291 private Map<String, SourcedValueSetEntry> vsCache = new HashMap<>(); 292 private Map<String, SourcedCodeSystemEntry> csCache = new HashMap<>(); 293 private Map<String, String> serverMap = new HashMap<>(); 294 @Getter @Setter private static boolean noCaching; 295 296 @Getter @Setter private static boolean cacheErrors; 297 298 299 // use lock from the context 300 public TerminologyCache(Object lock, String folder) throws FileNotFoundException, IOException, FHIRException { 301 super(); 302 this.lock = lock; 303 if (folder == null) { 304 folder = Utilities.path("[tmp]", "default-tx-cache"); 305 } 306 this.folder = folder; 307 requestCount = 0; 308 hitCount = 0; 309 networkCount = 0; 310 311 312 File f = ManagedFileAccess.file(folder); 313 if (!f.exists()) { 314 Utilities.createDirectory(folder); 315 } 316 if (!f.exists()) { 317 throw new IOException("Unable to create terminology cache at "+folder); 318 } 319 checkVersion(); 320 load(); 321 } 322 323 private void checkVersion() throws IOException { 324 File verFile = ManagedFileAccess.file(Utilities.path(folder, "version.ctl")); 325 if (verFile.exists()) { 326 String ver = TextFile.fileToString(verFile); 327 if (!ver.equals(FIXED_CACHE_VERSION)) { 328 System.out.println("Terminology Cache Version has changed from 1 to "+FIXED_CACHE_VERSION+", so clearing txCache"); 329 clear(); 330 } 331 TextFile.stringToFile(FIXED_CACHE_VERSION, verFile); 332 } else { 333 TextFile.stringToFile(FIXED_CACHE_VERSION, verFile); 334 } 335 } 336 337 public String getServerId(String address) throws IOException { 338 if (serverMap.containsKey(address)) { 339 return serverMap.get(address); 340 } 341 String id = address.replace("http://", "").replace("https://", "").replace("/", "."); 342 int i = 1; 343 while (serverMap.containsValue(id)) { 344 i++; 345 id = address.replace("https:", "").replace("https:", "").replace("/", ".")+i; 346 } 347 serverMap.put(address, id); 348 if (folder != null) { 349 IniFile ini = new IniFile(Utilities.path(folder, "servers.ini")); 350 ini.setStringProperty("servers", id, address, null); 351 ini.save(); 352 } 353 return id; 354 } 355 356 public void unload() { 357 // not useable after this is called 358 caches.clear(); 359 vsCache.clear(); 360 csCache.clear(); 361 } 362 363 private void clear() throws IOException { 364 Utilities.clearDirectory(folder); 365 caches.clear(); 366 vsCache.clear(); 367 csCache.clear(); 368 } 369 370 public boolean hasCapabilityStatement(String address) { 371 return capabilityStatementCache.containsKey(address); 372 } 373 374 public CapabilityStatement getCapabilityStatement(String address) { 375 return capabilityStatementCache.get(address); 376 } 377 378 public void cacheCapabilityStatement(String address, CapabilityStatement capabilityStatement) throws IOException { 379 if (noCaching) { 380 return; 381 } 382 this.capabilityStatementCache.put(address, capabilityStatement); 383 save(capabilityStatement, CAPABILITY_STATEMENT_TITLE+"."+getServerId(address)); 384 } 385 386 387 public boolean hasTerminologyCapabilities(String address) { 388 return terminologyCapabilitiesCache.containsKey(address); 389 } 390 391 public TerminologyCapabilities getTerminologyCapabilities(String address) { 392 return terminologyCapabilitiesCache.get(address); 393 } 394 395 public void cacheTerminologyCapabilities(String address, TerminologyCapabilities terminologyCapabilities) throws IOException { 396 if (noCaching) { 397 return; 398 } 399 this.terminologyCapabilitiesCache.put(address, terminologyCapabilities); 400 save(terminologyCapabilities, TERMINOLOGY_CAPABILITIES_TITLE+"."+getServerId(address)); 401 } 402 403 404 public CacheToken generateValidationToken(ValidationOptions options, Coding code, ValueSet vs, Parameters expParameters) { 405 try { 406 CacheToken ct = new CacheToken(); 407 if (code.hasSystem()) { 408 ct.setName(code.getSystem()); 409 ct.hasVersion = code.hasVersion(); 410 } 411 else 412 ct.name = NAME_FOR_NO_SYSTEM; 413 nameCacheToken(vs, ct); 414 JsonParser json = new JsonParser(); 415 json.setOutputStyle(OutputStyle.PRETTY); 416 String expJS = json.composeString(expParameters); 417 418 if (vs != null && vs.hasUrl() && vs.hasVersion()) { 419 ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+", \"url\": \""+Utilities.escapeJson(vs.getUrl()) 420 +"\", \"version\": \""+Utilities.escapeJson(vs.getVersion())+"\""+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}\r\n"; 421 } else if (options.getVsAsUrl()) { 422 ct.request = "{\"code\" : "+json.composeString(code, "code")+", \"valueSet\" :"+extracted(json, vs)+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}"; 423 } else { 424 ValueSet vsc = getVSEssense(vs); 425 ct.request = "{\"code\" : "+json.composeString(code, "code")+", \"valueSet\" :"+(vsc == null ? "null" : extracted(json, vsc))+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}"; 426 } 427 ct.key = String.valueOf(hashJson(ct.request)); 428 return ct; 429 } catch (IOException e) { 430 throw new Error(e); 431 } 432 } 433 434 public CacheToken generateValidationToken(ValidationOptions options, Coding code, String vsUrl, Parameters expParameters) { 435 try { 436 CacheToken ct = new CacheToken(); 437 if (code.hasSystem()) { 438 ct.setName(code.getSystem()); 439 ct.hasVersion = code.hasVersion(); 440 } else { 441 ct.name = NAME_FOR_NO_SYSTEM; 442 } 443 ct.setName(vsUrl); 444 JsonParser json = new JsonParser(); 445 json.setOutputStyle(OutputStyle.PRETTY); 446 String expJS = json.composeString(expParameters); 447 448 ct.request = "{\"code\" : "+json.composeString(code, "code")+", \"valueSet\" :"+(vsUrl == null ? "null" : vsUrl)+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}"; 449 ct.key = String.valueOf(hashJson(ct.request)); 450 return ct; 451 } catch (IOException e) { 452 throw new Error(e); 453 } 454 } 455 456 public String extracted(JsonParser json, ValueSet vsc) throws IOException { 457 String s = null; 458 if (vsc.getExpansion().getContains().size() > 1000 || vsc.getCompose().getIncludeFirstRep().getConcept().size() > 1000) { 459 s = vsc.getUrl(); 460 } else { 461 s = json.composeString(vsc); 462 } 463 return s; 464 } 465 466 public CacheToken generateValidationToken(ValidationOptions options, CodeableConcept code, ValueSet vs, Parameters expParameters) { 467 try { 468 CacheToken ct = new CacheToken(); 469 for (Coding c : code.getCoding()) { 470 if (c.hasSystem()) { 471 ct.setName(c.getSystem()); 472 ct.hasVersion = c.hasVersion(); 473 } 474 } 475 nameCacheToken(vs, ct); 476 JsonParser json = new JsonParser(); 477 json.setOutputStyle(OutputStyle.PRETTY); 478 String expJS = json.composeString(expParameters); 479 if (vs != null && vs.hasUrl() && vs.hasVersion()) { 480 ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+", \"url\": \""+Utilities.escapeJson(vs.getUrl())+ 481 "\", \"version\": \""+Utilities.escapeJson(vs.getVersion())+"\""+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}\r\n"; 482 } else if (vs == null) { 483 ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}"; 484 } else { 485 ValueSet vsc = getVSEssense(vs); 486 ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+", \"valueSet\" :"+extracted(json, vsc)+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}"; 487 } 488 ct.key = String.valueOf(hashJson(ct.request)); 489 return ct; 490 } catch (IOException e) { 491 throw new Error(e); 492 } 493 } 494 495 public ValueSet getVSEssense(ValueSet vs) { 496 if (vs == null) 497 return null; 498 ValueSet vsc = new ValueSet(); 499 vsc.setCompose(vs.getCompose()); 500 if (vs.hasExpansion()) { 501 vsc.getExpansion().getParameter().addAll(vs.getExpansion().getParameter()); 502 vsc.getExpansion().getContains().addAll(vs.getExpansion().getContains()); 503 } 504 return vsc; 505 } 506 507 public CacheToken generateExpandToken(ValueSet vs, boolean hierarchical) { 508 CacheToken ct = new CacheToken(); 509 nameCacheToken(vs, ct); 510 if (vs.hasUrl() && vs.hasVersion()) { 511 ct.request = "{\"hierarchical\" : "+(hierarchical ? "true" : "false")+", \"url\": \""+Utilities.escapeJson(vs.getUrl())+"\", \"version\": \""+Utilities.escapeJson(vs.getVersion())+"\"}\r\n"; 512 } else { 513 ValueSet vsc = getVSEssense(vs); 514 JsonParser json = new JsonParser(); 515 json.setOutputStyle(OutputStyle.PRETTY); 516 try { 517 ct.request = "{\"hierarchical\" : "+(hierarchical ? "true" : "false")+", \"valueSet\" :"+extracted(json, vsc)+"}\r\n"; 518 } catch (IOException e) { 519 throw new Error(e); 520 } 521 } 522 ct.key = String.valueOf(hashJson(ct.request)); 523 return ct; 524 } 525 526 public void nameCacheToken(ValueSet vs, CacheToken ct) { 527 if (vs != null) { 528 for (ConceptSetComponent inc : vs.getCompose().getInclude()) { 529 if (inc.hasSystem()) { 530 ct.setName(inc.getSystem()); 531 ct.hasVersion = inc.hasVersion(); 532 } 533 } 534 for (ConceptSetComponent inc : vs.getCompose().getExclude()) { 535 if (inc.hasSystem()) { 536 ct.setName(inc.getSystem()); 537 ct.hasVersion = inc.hasVersion(); 538 } 539 } 540 for (ValueSetExpansionContainsComponent inc : vs.getExpansion().getContains()) { 541 if (inc.hasSystem()) { 542 ct.setName(inc.getSystem()); 543 ct.hasVersion = inc.hasVersion(); 544 } 545 } 546 } 547 } 548 549 private String normalizeSystemPath(String path) { 550 return path.replace("/", "").replace('|','X'); 551 } 552 553 554 555 public NamedCache getNamedCache(CacheToken cacheToken) { 556 557 final String cacheName = cacheToken.name == null ? "null" : cacheToken.name; 558 559 NamedCache nc = caches.get(cacheName); 560 561 if (nc == null) { 562 nc = new NamedCache(); 563 nc.name = cacheName; 564 caches.put(nc.name, nc); 565 } 566 return nc; 567 } 568 569 public ValueSetExpansionOutcome getExpansion(CacheToken cacheToken) { 570 synchronized (lock) { 571 NamedCache nc = getNamedCache(cacheToken); 572 CacheEntry e = nc.map.get(cacheToken.key); 573 if (e == null) 574 return null; 575 else 576 return e.e; 577 } 578 } 579 580 public void cacheExpansion(CacheToken cacheToken, ValueSetExpansionOutcome res, boolean persistent) { 581 synchronized (lock) { 582 NamedCache nc = getNamedCache(cacheToken); 583 CacheEntry e = new CacheEntry(); 584 e.request = cacheToken.request; 585 e.persistent = persistent; 586 e.e = res; 587 store(cacheToken, persistent, nc, e); 588 } 589 } 590 591 public void store(CacheToken cacheToken, boolean persistent, NamedCache nc, CacheEntry e) { 592 if (noCaching) { 593 return; 594 } 595 596 if ( !cacheErrors && 597 ( e.v!= null 598 && e.v.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED 599 && !cacheToken.hasVersion)) { 600 return; 601 } 602 603 boolean n = nc.map.containsKey(cacheToken.key); 604 nc.map.put(cacheToken.key, e); 605 if (persistent) { 606 if (n) { 607 for (int i = nc.list.size()- 1; i>= 0; i--) { 608 if (nc.list.get(i).request.equals(e.request)) { 609 nc.list.remove(i); 610 } 611 } 612 } 613 nc.list.add(e); 614 save(nc); 615 } 616 } 617 618 public ValidationResult getValidation(CacheToken cacheToken) { 619 if (cacheToken.key == null) { 620 return null; 621 } 622 synchronized (lock) { 623 requestCount++; 624 NamedCache nc = getNamedCache(cacheToken); 625 CacheEntry e = nc.map.get(cacheToken.key); 626 if (e == null) { 627 networkCount++; 628 return null; 629 } else { 630 hitCount++; 631 return new ValidationResult(e.v); 632 } 633 } 634 } 635 636 public void cacheValidation(CacheToken cacheToken, ValidationResult res, boolean persistent) { 637 if (cacheToken.key != null) { 638 synchronized (lock) { 639 NamedCache nc = getNamedCache(cacheToken); 640 CacheEntry e = new CacheEntry(); 641 e.request = cacheToken.request; 642 e.persistent = persistent; 643 e.v = new ValidationResult(res); 644 store(cacheToken, persistent, nc, e); 645 } 646 } 647 } 648 649 650 // persistence 651 652 public void save() { 653 654 } 655 656 private <K extends Resource> void save(K resource, String title) { 657 if (folder == null) 658 return; 659 660 try { 661 OutputStreamWriter sw = new OutputStreamWriter(ManagedFileAccess.outStream(Utilities.path(folder, title + CACHE_FILE_EXTENSION)), "UTF-8"); 662 663 JsonParser json = new JsonParser(); 664 json.setOutputStyle(OutputStyle.PRETTY); 665 666 sw.write(json.composeString(resource).trim()); 667 sw.close(); 668 } catch (Exception e) { 669 System.out.println("error saving capability statement "+e.getMessage()); 670 } 671 } 672 673 private void save(NamedCache nc) { 674 if (folder == null) 675 return; 676 677 try { 678 OutputStreamWriter sw = new OutputStreamWriter(ManagedFileAccess.outStream(Utilities.path(folder, nc.name+CACHE_FILE_EXTENSION)), "UTF-8"); 679 sw.write(ENTRY_MARKER+"\r\n"); 680 JsonParser json = new JsonParser(); 681 json.setOutputStyle(OutputStyle.PRETTY); 682 for (CacheEntry ce : nc.list) { 683 sw.write(ce.request.trim()); 684 sw.write(BREAK+"\r\n"); 685 if (ce.e != null) { 686 sw.write("e: {\r\n"); 687 if (ce.e.isFromServer()) 688 sw.write(" \"from-server\" : true,\r\n"); 689 if (ce.e.getValueset() != null) 690 sw.write(" \"valueSet\" : "+json.composeString(ce.e.getValueset()).trim()+",\r\n"); 691 sw.write(" \"error\" : \""+Utilities.escapeJson(ce.e.getError()).trim()+"\"\r\n}\r\n"); 692 } else if (ce.s != null) { 693 sw.write("s: {\r\n"); 694 sw.write(" \"result\" : "+ce.s.result+"\r\n}\r\n"); 695 } else { 696 sw.write("v: {\r\n"); 697 boolean first = true; 698 if (ce.v.getDisplay() != null) { 699 if (first) first = false; else sw.write(",\r\n"); 700 sw.write(" \"display\" : \""+Utilities.escapeJson(ce.v.getDisplay()).trim()+"\""); 701 } 702 if (ce.v.getCode() != null) { 703 if (first) first = false; else sw.write(",\r\n"); 704 sw.write(" \"code\" : \""+Utilities.escapeJson(ce.v.getCode()).trim()+"\""); 705 } 706 if (ce.v.getSystem() != null) { 707 if (first) first = false; else sw.write(",\r\n"); 708 sw.write(" \"system\" : \""+Utilities.escapeJson(ce.v.getSystem()).trim()+"\""); 709 } 710 if (ce.v.getVersion() != null) { 711 if (first) first = false; else sw.write(",\r\n"); 712 sw.write(" \"version\" : \""+Utilities.escapeJson(ce.v.getVersion()).trim()+"\""); 713 } 714 if (ce.v.getSeverity() != null) { 715 if (first) first = false; else sw.write(",\r\n"); 716 sw.write(" \"severity\" : "+"\""+ce.v.getSeverity().toCode().trim()+"\""+""); 717 } 718 if (ce.v.getMessage() != null) { 719 if (first) first = false; else sw.write(",\r\n"); 720 sw.write(" \"error\" : \""+Utilities.escapeJson(ce.v.getMessage()).trim()+"\""); 721 } 722 if (ce.v.getErrorClass() != null) { 723 if (first) first = false; else sw.write(",\r\n"); 724 sw.write(" \"class\" : \""+Utilities.escapeJson(ce.v.getErrorClass().toString())+"\""); 725 } 726 if (ce.v.getDefinition() != null) { 727 if (first) first = false; else sw.write(",\r\n"); 728 sw.write(" \"definition\" : \""+Utilities.escapeJson(ce.v.getDefinition()).trim()+"\""); 729 } 730 if (ce.v.getStatus() != null) { 731 if (first) first = false; else sw.write(",\r\n"); 732 sw.write(" \"status\" : \""+Utilities.escapeJson(ce.v.getStatus()).trim()+"\""); 733 } 734 if (ce.v.getServer() != null) { 735 if (first) first = false; else sw.write(",\r\n"); 736 sw.write(" \"server\" : \""+Utilities.escapeJson(ce.v.getServer()).trim()+"\""); 737 } 738 if (ce.v.isInactive()) { 739 if (first) first = false; else sw.write(",\r\n"); 740 sw.write(" \"inactive\" : true"); 741 } 742 if (ce.v.getUnknownSystems() != null) { 743 if (first) first = false; else sw.write(",\r\n"); 744 sw.write(" \"unknown-systems\" : \""+Utilities.escapeJson(CommaSeparatedStringBuilder.join(",", ce.v.getUnknownSystems())).trim()+"\""); 745 } 746 if (ce.v.getIssues() != null) { 747 if (first) first = false; else sw.write(",\r\n"); 748 OperationOutcome oo = new OperationOutcome(); 749 oo.setIssue(ce.v.getIssues()); 750 sw.write(" \"issues\" : "+json.composeString(oo).trim()+"\r\n"); 751 } 752 sw.write("\r\n}\r\n"); 753 } 754 sw.write(ENTRY_MARKER+"\r\n"); 755 } 756 sw.close(); 757 } catch (Exception e) { 758 System.out.println("error saving "+nc.name+": "+e.getMessage()); 759 } 760 } 761 762 private boolean isCapabilityCache(String fn) { 763 if (fn == null) { 764 return false; 765 } 766 return fn.startsWith(CAPABILITY_STATEMENT_TITLE) || fn.startsWith(TERMINOLOGY_CAPABILITIES_TITLE); 767 } 768 769 private void loadCapabilityCache(String fn) { 770 try { 771 String src = TextFile.fileToString(Utilities.path(folder, fn)); 772 String serverId = Utilities.getFileNameForName(fn).replace(CACHE_FILE_EXTENSION, ""); 773 serverId = serverId.substring(serverId.indexOf(".")+1); 774 serverId = serverId.substring(serverId.indexOf(".")+1); 775 String address = getServerForId(serverId); 776 if (address != null) { 777 JsonObject o = (JsonObject) new com.google.gson.JsonParser().parse(src); 778 Resource resource = new JsonParser().parse(o); 779 780 if (fn.startsWith(CAPABILITY_STATEMENT_TITLE)) { 781 this.capabilityStatementCache.put(address, (CapabilityStatement) resource); 782 } else if (fn.startsWith(TERMINOLOGY_CAPABILITIES_TITLE)) { 783 this.terminologyCapabilitiesCache.put(address, (TerminologyCapabilities) resource); 784 } 785 } 786 } catch (Exception e) { 787 e.printStackTrace(); 788 throw new FHIRException("Error loading " + fn + ": " + e.getMessage(), e); 789 } 790 } 791 792 private String getServerForId(String serverId) { 793 for (String n : serverMap.keySet()) { 794 if (serverMap.get(n).equals(serverId)) { 795 return n; 796 } 797 } 798 return null; 799 } 800 801 private CacheEntry getCacheEntry(String request, String resultString) throws IOException { 802 CacheEntry ce = new CacheEntry(); 803 ce.persistent = true; 804 ce.request = request; 805 char e = resultString.charAt(0); 806 resultString = resultString.substring(3); 807 JsonObject o = (JsonObject) new com.google.gson.JsonParser().parse(resultString); 808 String error = loadJS(o.get("error")); 809 if (e == 'e') { 810 if (o.has("valueSet")) 811 ce.e = new ValueSetExpansionOutcome((ValueSet) new JsonParser().parse(o.getAsJsonObject("valueSet")), error, TerminologyServiceErrorClass.UNKNOWN, o.has("from-server")); 812 else 813 ce.e = new ValueSetExpansionOutcome(error, TerminologyServiceErrorClass.UNKNOWN, o.has("from-server")); 814 } else if (e == 's') { 815 ce.s = new SubsumesResult(o.get("result").getAsBoolean()); 816 } else { 817 String t = loadJS(o.get("severity")); 818 IssueSeverity severity = t == null ? null : IssueSeverity.fromCode(t); 819 String display = loadJS(o.get("display")); 820 String code = loadJS(o.get("code")); 821 String system = loadJS(o.get("system")); 822 String version = loadJS(o.get("version")); 823 String definition = loadJS(o.get("definition")); 824 String server = loadJS(o.get("server")); 825 String status = loadJS(o.get("status")); 826 boolean inactive = "true".equals(loadJS(o.get("inactive"))); 827 String unknownSystems = loadJS(o.get("unknown-systems")); 828 OperationOutcome oo = o.has("issues") ? (OperationOutcome) new JsonParser().parse(o.getAsJsonObject("issues")) : null; 829 t = loadJS(o.get("class")); 830 TerminologyServiceErrorClass errorClass = t == null ? null : TerminologyServiceErrorClass.valueOf(t) ; 831 ce.v = new ValidationResult(severity, error, system, version, new ConceptDefinitionComponent().setDisplay(display).setDefinition(definition).setCode(code), display, null).setErrorClass(errorClass); 832 ce.v.setUnknownSystems(CommaSeparatedStringBuilder.toSet(unknownSystems)); 833 ce.v.setServer(server); 834 ce.v.setStatus(inactive, status); 835 if (oo != null) { 836 ce.v.setIssues(oo.getIssue()); 837 } 838 } 839 return ce; 840 } 841 842 private void loadNamedCache(String fn) { 843 int c = 0; 844 try { 845 String src = TextFile.fileToString(Utilities.path(folder, fn)); 846 String title = fn.substring(0, fn.lastIndexOf(".")); 847 848 NamedCache nc = new NamedCache(); 849 nc.name = title; 850 851 if (src.startsWith("?")) 852 src = src.substring(1); 853 int i = src.indexOf(ENTRY_MARKER); 854 while (i > -1) { 855 c++; 856 String s = src.substring(0, i); 857 src = src.substring(i + ENTRY_MARKER.length() + 1); 858 i = src.indexOf(ENTRY_MARKER); 859 if (!Utilities.noString(s)) { 860 int j = s.indexOf(BREAK); 861 String request = s.substring(0, j); 862 String p = s.substring(j + BREAK.length() + 1).trim(); 863 864 CacheEntry cacheEntry = getCacheEntry(request, p); 865 866 nc.map.put(String.valueOf(hashJson(cacheEntry.request)), cacheEntry); 867 nc.list.add(cacheEntry); 868 } 869 caches.put(nc.name, nc); 870 } 871 } catch (Exception e) { 872 System.out.println("Error loading "+fn+": "+e.getMessage()+" entry "+c+" - ignoring it"); 873 e.printStackTrace(); 874 } 875 } 876 877 private void load() throws FHIRException, IOException { 878 IniFile ini = new IniFile(Utilities.path(folder, "servers.ini")); 879 if (ini.hasSection("servers")) { 880 for (String n : ini.getPropertyNames("servers")) { 881 serverMap.put(ini.getStringProperty("servers", n), n); 882 } 883 } 884 885 for (String fn : ManagedFileAccess.file(folder).list()) { 886 if (fn.endsWith(CACHE_FILE_EXTENSION) && !fn.equals("validation" + CACHE_FILE_EXTENSION)) { 887 try { 888 if (isCapabilityCache(fn)) { 889 loadCapabilityCache(fn); 890 } else { 891 loadNamedCache(fn); 892 } 893 } catch (FHIRException e) { 894 throw e; 895 } 896 } 897 } 898 try { 899 File f = ManagedFileAccess.file(Utilities.path(folder, "vs-externals.json")); 900 if (f.exists()) { 901 org.hl7.fhir.utilities.json.model.JsonObject json = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(f); 902 for (JsonProperty p : json.getProperties()) { 903 if (p.getValue().isJsonNull()) { 904 vsCache.put(p.getName(), null); 905 } else { 906 org.hl7.fhir.utilities.json.model.JsonObject j = p.getValue().asJsonObject(); 907 vsCache.put(p.getName(), new SourcedValueSetEntry(j.asString("server"), j.asString("filename"))); 908 } 909 } 910 } 911 } catch (Exception e) { 912 System.out.println("Error loading vs external cache: "+e.getMessage()); 913 } 914 try { 915 File f = ManagedFileAccess.file(Utilities.path(folder, "cs-externals.json")); 916 if (f.exists()) { 917 org.hl7.fhir.utilities.json.model.JsonObject json = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(f); 918 for (JsonProperty p : json.getProperties()) { 919 if (p.getValue().isJsonNull()) { 920 csCache.put(p.getName(), null); 921 } else { 922 org.hl7.fhir.utilities.json.model.JsonObject j = p.getValue().asJsonObject(); 923 csCache.put(p.getName(), new SourcedCodeSystemEntry(j.asString("server"), j.asString("filename"))); 924 } 925 } 926 } 927 } catch (Exception e) { 928 System.out.println("Error loading vs external cache: "+e.getMessage()); 929 } 930 } 931 932 private String loadJS(JsonElement e) { 933 if (e == null) 934 return null; 935 if (!(e instanceof JsonPrimitive)) 936 return null; 937 String s = e.getAsString(); 938 if ("".equals(s)) 939 return null; 940 return s; 941 } 942 943 public String hashJson(String s) { 944 return String.valueOf(s.trim().hashCode()); 945 } 946 947 // management 948 949 public String summary(ValueSet vs) { 950 if (vs == null) 951 return "null"; 952 953 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 954 for (ConceptSetComponent cc : vs.getCompose().getInclude()) 955 b.append("Include "+getIncSummary(cc)); 956 for (ConceptSetComponent cc : vs.getCompose().getExclude()) 957 b.append("Exclude "+getIncSummary(cc)); 958 return b.toString(); 959 } 960 961 private String getIncSummary(ConceptSetComponent cc) { 962 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 963 for (UriType vs : cc.getValueSet()) 964 b.append(vs.asStringValue()); 965 String vsd = b.length() > 0 ? " where the codes are in the value sets ("+b.toString()+")" : ""; 966 String system = cc.getSystem(); 967 if (cc.hasConcept()) 968 return Integer.toString(cc.getConcept().size())+" codes from "+system+vsd; 969 if (cc.hasFilter()) { 970 String s = ""; 971 for (ConceptSetFilterComponent f : cc.getFilter()) { 972 if (!Utilities.noString(s)) 973 s = s + " & "; 974 s = s + f.getProperty()+" "+(f.hasOp() ? f.getOp().toCode() : "?")+" "+f.getValue(); 975 } 976 return "from "+system+" where "+s+vsd; 977 } 978 return "All codes from "+system+vsd; 979 } 980 981 public String summary(Coding code) { 982 return code.getSystem()+"#"+code.getCode()+(code.hasDisplay() ? ": \""+code.getDisplay()+"\"" : ""); 983 } 984 985 public String summary(CodeableConcept code) { 986 StringBuilder b = new StringBuilder(); 987 b.append("{"); 988 boolean first = true; 989 for (Coding c : code.getCoding()) { 990 if (first) first = false; else b.append(","); 991 b.append(summary(c)); 992 } 993 b.append("}: \""); 994 b.append(code.getText()); 995 b.append("\""); 996 return b.toString(); 997 } 998 999 public void removeCS(String url) { 1000 synchronized (lock) { 1001 String name = getSystemNameKeyGenerator().getNameForSystem(url); 1002 if (caches.containsKey(name)) { 1003 caches.remove(name); 1004 } 1005 } 1006 } 1007 1008 public String getFolder() { 1009 return folder; 1010 } 1011 1012 public Map<String, String> servers() { 1013 Map<String, String> servers = new HashMap<>(); 1014// servers.put("http://local.fhir.org/r2", "tx.fhir.org"); 1015// servers.put("http://local.fhir.org/r3", "tx.fhir.org"); 1016// servers.put("http://local.fhir.org/r4", "tx.fhir.org"); 1017// servers.put("http://local.fhir.org/r5", "tx.fhir.org"); 1018// 1019// servers.put("http://tx-dev.fhir.org/r2", "tx.fhir.org"); 1020// servers.put("http://tx-dev.fhir.org/r3", "tx.fhir.org"); 1021// servers.put("http://tx-dev.fhir.org/r4", "tx.fhir.org"); 1022// servers.put("http://tx-dev.fhir.org/r5", "tx.fhir.org"); 1023 1024 servers.put("http://tx.fhir.org/r2", "tx.fhir.org"); 1025 servers.put("http://tx.fhir.org/r3", "tx.fhir.org"); 1026 servers.put("http://tx.fhir.org/r4", "tx.fhir.org"); 1027 servers.put("http://tx.fhir.org/r5", "tx.fhir.org"); 1028 1029 return servers; 1030 } 1031 1032 public boolean hasValueSet(String canonical) { 1033 return vsCache.containsKey(canonical); 1034 } 1035 1036 public boolean hasCodeSystem(String canonical) { 1037 return csCache.containsKey(canonical); 1038 } 1039 1040 public SourcedValueSet getValueSet(String canonical) { 1041 SourcedValueSetEntry sp = vsCache.get(canonical); 1042 if (sp == null) { 1043 return null; 1044 } else { 1045 try { 1046 return new SourcedValueSet(sp.getServer(), sp.getFilename() == null ? null : (ValueSet) new JsonParser().parse(ManagedFileAccess.inStream(Utilities.path(folder, sp.getFilename())))); 1047 } catch (Exception e) { 1048 return null; 1049 } 1050 } 1051 } 1052 1053 public SourcedCodeSystem getCodeSystem(String canonical) { 1054 SourcedCodeSystemEntry sp = csCache.get(canonical); 1055 if (sp == null) { 1056 return null; 1057 } else { 1058 try { 1059 return new SourcedCodeSystem(sp.getServer(), sp.getFilename() == null ? null : (CodeSystem) new JsonParser().parse(ManagedFileAccess.inStream(Utilities.path(folder, sp.getFilename())))); 1060 } catch (Exception e) { 1061 return null; 1062 } 1063 } 1064 } 1065 1066 public void cacheValueSet(String canonical, SourcedValueSet svs) { 1067 if (canonical == null) { 1068 return; 1069 } 1070 try { 1071 if (svs == null) { 1072 vsCache.put(canonical, null); 1073 } else { 1074 String uuid = Utilities.makeUuidLC(); 1075 String fn = "vs-"+uuid+".json"; 1076 new JsonParser().compose(ManagedFileAccess.outStream(Utilities.path(folder, fn)), svs.getVs()); 1077 vsCache.put(canonical, new SourcedValueSetEntry(svs.getServer(), fn)); 1078 } 1079 org.hl7.fhir.utilities.json.model.JsonObject j = new org.hl7.fhir.utilities.json.model.JsonObject(); 1080 for (String k : vsCache.keySet()) { 1081 SourcedValueSetEntry sve = vsCache.get(k); 1082 if (sve == null) { 1083 j.add(k, new JsonNull()); 1084 } else { 1085 org.hl7.fhir.utilities.json.model.JsonObject e = new org.hl7.fhir.utilities.json.model.JsonObject(); 1086 e.set("server", sve.getServer()); 1087 if (sve.getFilename() != null) { 1088 e.set("filename", sve.getFilename()); 1089 } 1090 j.add(k, e); 1091 } 1092 } 1093 org.hl7.fhir.utilities.json.parser.JsonParser.compose(j, ManagedFileAccess.file(Utilities.path(folder, "vs-externals.json")), true); 1094 } catch (Exception e) { 1095 e.printStackTrace(); 1096 } 1097 } 1098 1099 public void cacheCodeSystem(String canonical, SourcedCodeSystem scs) { 1100 if (canonical == null) { 1101 return; 1102 } 1103 try { 1104 if (scs == null) { 1105 csCache.put(canonical, null); 1106 } else { 1107 String uuid = Utilities.makeUuidLC(); 1108 String fn = "cs-"+uuid+".json"; 1109 new JsonParser().compose(ManagedFileAccess.outStream(Utilities.path(folder, fn)), scs.getCs()); 1110 csCache.put(canonical, new SourcedCodeSystemEntry(scs.getServer(), fn)); 1111 } 1112 org.hl7.fhir.utilities.json.model.JsonObject j = new org.hl7.fhir.utilities.json.model.JsonObject(); 1113 for (String k : csCache.keySet()) { 1114 SourcedCodeSystemEntry sve = csCache.get(k); 1115 if (sve == null) { 1116 j.add(k, new JsonNull()); 1117 } else { 1118 org.hl7.fhir.utilities.json.model.JsonObject e = new org.hl7.fhir.utilities.json.model.JsonObject(); 1119 e.set("server", sve.getServer()); 1120 if (sve.getFilename() != null) { 1121 e.set("filename", sve.getFilename()); 1122 } 1123 j.add(k, e); 1124 } 1125 } 1126 org.hl7.fhir.utilities.json.parser.JsonParser.compose(j, ManagedFileAccess.file(Utilities.path(folder, "cs-externals.json")), true); 1127 } catch (Exception e) { 1128 e.printStackTrace(); 1129 } 1130 } 1131 1132 public CacheToken generateSubsumesToken(ValidationOptions options, Coding parent, Coding child, Parameters expParameters) { 1133 try { 1134 CacheToken ct = new CacheToken(); 1135 if (parent.hasSystem()) { 1136 ct.setName(parent.getSystem()); 1137 } 1138 if (child.hasSystem()) { 1139 ct.setName(child.getSystem()); 1140 } 1141 ct.hasVersion = parent.hasVersion() || child.hasVersion(); 1142 JsonParser json = new JsonParser(); 1143 json.setOutputStyle(OutputStyle.PRETTY); 1144 String expJS = json.composeString(expParameters); 1145 ct.request = "{\"op\": \"subsumes\", \"parent\" : "+json.composeString(parent, "code")+", \"child\" :"+json.composeString(child, "code")+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}"; 1146 ct.key = String.valueOf(hashJson(ct.request)); 1147 return ct; 1148 } catch (IOException e) { 1149 throw new Error(e); 1150 } 1151 } 1152 1153 public Boolean getSubsumes(CacheToken cacheToken) { 1154 if (cacheToken.key == null) { 1155 return null; 1156 } 1157 synchronized (lock) { 1158 requestCount++; 1159 NamedCache nc = getNamedCache(cacheToken); 1160 CacheEntry e = nc.map.get(cacheToken.key); 1161 if (e == null) { 1162 networkCount++; 1163 return null; 1164 } else { 1165 hitCount++; 1166 return e.s.result; 1167 } 1168 } 1169 1170 } 1171 1172 public void cacheSubsumes(CacheToken cacheToken, Boolean b, boolean persistent) { 1173 if (cacheToken.key != null) { 1174 synchronized (lock) { 1175 NamedCache nc = getNamedCache(cacheToken); 1176 CacheEntry e = new CacheEntry(); 1177 e.request = cacheToken.request; 1178 e.persistent = persistent; 1179 e.s = new SubsumesResult(b); 1180 store(cacheToken, persistent, nc, e); 1181 } 1182 } 1183 } 1184 1185 1186}