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    } else if ("n/a".equals(folder)) {
306      // this is a weird way to do things but it maintains the legacy interface
307      folder = null;
308    }
309    this.folder = folder;
310    requestCount = 0;
311    hitCount = 0;
312    networkCount = 0;
313
314    if (folder != null) {
315      File f = ManagedFileAccess.file(folder);
316      if (!f.exists()) {
317        Utilities.createDirectory(folder);
318      }
319      if (!f.exists()) {
320        throw new IOException("Unable to create terminology cache at "+folder);
321      }
322      checkVersion();      
323      load();
324    }
325  }
326
327  private void checkVersion() throws IOException {
328    File verFile = ManagedFileAccess.file(Utilities.path(folder, "version.ctl"));
329    if (verFile.exists()) {
330      String ver = TextFile.fileToString(verFile);
331      if (!ver.equals(FIXED_CACHE_VERSION)) {
332        System.out.println("Terminology Cache Version has changed from 1 to "+FIXED_CACHE_VERSION+", so clearing txCache");
333        clear();
334      }
335      TextFile.stringToFile(FIXED_CACHE_VERSION, verFile);
336    } else {
337      TextFile.stringToFile(FIXED_CACHE_VERSION, verFile);
338    }
339  }
340
341  public String getServerId(String address) throws IOException  {
342    if (serverMap.containsKey(address)) {
343      return serverMap.get(address);
344    }
345    String id = address.replace("http://", "").replace("https://", "").replace("/", ".");
346    int i = 1;
347    while (serverMap.containsValue(id)) {
348      i++;
349      id =  address.replace("https:", "").replace("https:", "").replace("/", ".")+i;
350    }
351    serverMap.put(address, id);
352    if (folder != null) {
353      IniFile ini = new IniFile(Utilities.path(folder, "servers.ini"));
354      ini.setStringProperty("servers", id, address, null);
355      ini.save();
356    }
357    return id;
358  }
359  
360  public void unload() {
361    // not useable after this is called
362    caches.clear();
363    vsCache.clear();
364    csCache.clear();
365  }
366  
367  public void clear() throws IOException {
368    if (folder != null) {
369      Utilities.clearDirectory(folder);
370    }
371    caches.clear();
372    vsCache.clear();
373    csCache.clear();
374  }
375  
376  public boolean hasCapabilityStatement(String address) {
377    return capabilityStatementCache.containsKey(address);
378  }
379
380  public CapabilityStatement getCapabilityStatement(String address) {
381    return capabilityStatementCache.get(address);
382  }
383
384  public void cacheCapabilityStatement(String address, CapabilityStatement capabilityStatement) throws IOException {
385    if (noCaching) {
386      return;
387    } 
388    this.capabilityStatementCache.put(address, capabilityStatement);
389    save(capabilityStatement, CAPABILITY_STATEMENT_TITLE+"."+getServerId(address));
390  }
391
392
393  public boolean hasTerminologyCapabilities(String address) {
394    return terminologyCapabilitiesCache.containsKey(address);
395  }
396
397  public TerminologyCapabilities getTerminologyCapabilities(String address) {
398    return terminologyCapabilitiesCache.get(address);
399  }
400
401  public void cacheTerminologyCapabilities(String address, TerminologyCapabilities terminologyCapabilities) throws IOException {
402    if (noCaching) {
403      return;
404    }
405    this.terminologyCapabilitiesCache.put(address, terminologyCapabilities);
406    save(terminologyCapabilities, TERMINOLOGY_CAPABILITIES_TITLE+"."+getServerId(address));
407  }
408
409
410  public CacheToken generateValidationToken(ValidationOptions options, Coding code, ValueSet vs, Parameters expParameters) {
411    try {
412      CacheToken ct = new CacheToken();
413      if (code.hasSystem()) {
414        ct.setName(code.getSystem());
415        ct.hasVersion = code.hasVersion();
416      }
417      else
418        ct.name = NAME_FOR_NO_SYSTEM;
419      nameCacheToken(vs, ct);
420      JsonParser json = new JsonParser();
421      json.setOutputStyle(OutputStyle.PRETTY);
422      String expJS = json.composeString(expParameters);
423
424      if (vs != null && vs.hasUrl() && vs.hasVersion()) {
425        ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+", \"url\": \""+Utilities.escapeJson(vs.getUrl())
426        +"\", \"version\": \""+Utilities.escapeJson(vs.getVersion())+"\""+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}\r\n";
427      } else if (options.getVsAsUrl()) {
428        ct.request = "{\"code\" : "+json.composeString(code, "code")+", \"valueSet\" :"+extracted(json, vs)+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}";
429      } else {
430        ValueSet vsc = getVSEssense(vs);
431        ct.request = "{\"code\" : "+json.composeString(code, "code")+", \"valueSet\" :"+(vsc == null ? "null" : extracted(json, vsc))+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}";
432      }
433      ct.key = String.valueOf(hashJson(ct.request));
434      return ct;
435    } catch (IOException e) {
436      throw new Error(e);
437    }
438  }
439
440  public CacheToken generateValidationToken(ValidationOptions options, Coding code, String vsUrl, Parameters expParameters) {
441    try {
442      CacheToken ct = new CacheToken();
443      if (code.hasSystem()) {
444        ct.setName(code.getSystem());
445        ct.hasVersion = code.hasVersion();
446      } else {
447        ct.name = NAME_FOR_NO_SYSTEM;
448      }
449      ct.setName(vsUrl);
450      JsonParser json = new JsonParser();
451      json.setOutputStyle(OutputStyle.PRETTY);
452      String expJS = json.composeString(expParameters);
453
454      ct.request = "{\"code\" : "+json.composeString(code, "code")+", \"valueSet\" :"+(vsUrl == null ? "null" : vsUrl)+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}";
455      ct.key = String.valueOf(hashJson(ct.request));
456      return ct;
457    } catch (IOException e) {
458      throw new Error(e);
459    }
460  }
461
462  public String extracted(JsonParser json, ValueSet vsc) throws IOException {
463    String s = null;
464    if (vsc.getExpansion().getContains().size() > 1000 || vsc.getCompose().getIncludeFirstRep().getConcept().size() > 1000) {      
465      s =  vsc.getUrl();
466    } else {
467      s = json.composeString(vsc);
468    }
469    return s;
470  }
471
472  public CacheToken generateValidationToken(ValidationOptions options, CodeableConcept code, ValueSet vs, Parameters expParameters) {
473    try {
474      CacheToken ct = new CacheToken();
475      for (Coding c : code.getCoding()) {
476        if (c.hasSystem()) {
477          ct.setName(c.getSystem());
478          ct.hasVersion = c.hasVersion();
479        }
480      }
481      nameCacheToken(vs, ct);
482      JsonParser json = new JsonParser();
483      json.setOutputStyle(OutputStyle.PRETTY);
484      String expJS = json.composeString(expParameters);
485      if (vs != null && vs.hasUrl() && vs.hasVersion()) {
486        ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+", \"url\": \""+Utilities.escapeJson(vs.getUrl())+
487            "\", \"version\": \""+Utilities.escapeJson(vs.getVersion())+"\""+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}\r\n";      
488      } else if (vs == null) { 
489        ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}";        
490      } else {
491        ValueSet vsc = getVSEssense(vs);
492        ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+", \"valueSet\" :"+extracted(json, vsc)+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}";
493      }
494      ct.key = String.valueOf(hashJson(ct.request));
495      return ct;
496    } catch (IOException e) {
497      throw new Error(e);
498    }
499  }
500
501  public ValueSet getVSEssense(ValueSet vs) {
502    if (vs == null)
503      return null;
504    ValueSet vsc = new ValueSet();
505    vsc.setCompose(vs.getCompose());
506    if (vs.hasExpansion()) {
507      vsc.getExpansion().getParameter().addAll(vs.getExpansion().getParameter());
508      vsc.getExpansion().getContains().addAll(vs.getExpansion().getContains());
509    }
510    return vsc;
511  }
512
513  public CacheToken generateExpandToken(ValueSet vs, boolean hierarchical) {
514    CacheToken ct = new CacheToken();
515    nameCacheToken(vs, ct);
516    if (vs.hasUrl() && vs.hasVersion()) {
517      ct.request = "{\"hierarchical\" : "+(hierarchical ? "true" : "false")+", \"url\": \""+Utilities.escapeJson(vs.getUrl())+"\", \"version\": \""+Utilities.escapeJson(vs.getVersion())+"\"}\r\n";      
518    } else {
519      ValueSet vsc = getVSEssense(vs);
520      JsonParser json = new JsonParser();
521      json.setOutputStyle(OutputStyle.PRETTY);
522      try {
523        ct.request = "{\"hierarchical\" : "+(hierarchical ? "true" : "false")+", \"valueSet\" :"+extracted(json, vsc)+"}\r\n";
524      } catch (IOException e) {
525        throw new Error(e);
526      }
527    }
528    ct.key = String.valueOf(hashJson(ct.request));
529    return ct;
530  }
531
532  public void nameCacheToken(ValueSet vs, CacheToken ct) {
533    if (vs != null) {
534      for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
535        if (inc.hasSystem()) {
536          ct.setName(inc.getSystem());
537          ct.hasVersion = inc.hasVersion();
538        }
539      }
540      for (ConceptSetComponent inc : vs.getCompose().getExclude()) {
541        if (inc.hasSystem()) {
542          ct.setName(inc.getSystem());
543          ct.hasVersion = inc.hasVersion();
544        }
545      }
546      for (ValueSetExpansionContainsComponent inc : vs.getExpansion().getContains()) {
547        if (inc.hasSystem()) {
548          ct.setName(inc.getSystem());
549          ct.hasVersion = inc.hasVersion();
550        }
551      }
552    }
553  }
554
555  private String normalizeSystemPath(String path) {
556    return path.replace("/", "").replace('|','X');
557  }
558
559
560
561  public NamedCache getNamedCache(CacheToken cacheToken) {
562
563    final String cacheName = cacheToken.name == null ? "null" : cacheToken.name;
564
565    NamedCache nc = caches.get(cacheName);
566
567    if (nc == null) {
568      nc = new NamedCache();
569      nc.name = cacheName;
570      caches.put(nc.name, nc);
571    }
572    return nc;
573  }
574
575  public ValueSetExpansionOutcome getExpansion(CacheToken cacheToken) {
576    synchronized (lock) {
577      NamedCache nc = getNamedCache(cacheToken);
578      CacheEntry e = nc.map.get(cacheToken.key);
579      if (e == null)
580        return null;
581      else
582        return e.e;
583    }
584  }
585
586  public void cacheExpansion(CacheToken cacheToken, ValueSetExpansionOutcome res, boolean persistent) {
587    synchronized (lock) {      
588      NamedCache nc = getNamedCache(cacheToken);
589      CacheEntry e = new CacheEntry();
590      e.request = cacheToken.request;
591      e.persistent = persistent;
592      e.e = res;
593      store(cacheToken, persistent, nc, e);
594    }    
595  }
596
597  public void store(CacheToken cacheToken, boolean persistent, NamedCache nc, CacheEntry e) {
598    if (noCaching) {
599      return;
600    }
601
602    if ( !cacheErrors &&
603        ( e.v!= null
604        && e.v.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED
605        && !cacheToken.hasVersion)) {
606      return;
607    }
608
609    boolean n = nc.map.containsKey(cacheToken.key);
610    nc.map.put(cacheToken.key, e);
611    if (persistent) {
612      if (n) {
613        for (int i = nc.list.size()- 1; i>= 0; i--) {
614          if (nc.list.get(i).request.equals(e.request)) {
615            nc.list.remove(i);
616          }
617        }
618      }
619      nc.list.add(e);
620      save(nc);  
621    }
622  }
623
624  public ValidationResult getValidation(CacheToken cacheToken) {
625    if (cacheToken.key == null) {
626      return null;
627    }
628    synchronized (lock) {
629      requestCount++;
630      NamedCache nc = getNamedCache(cacheToken);
631      CacheEntry e = nc.map.get(cacheToken.key);
632      if (e == null) {
633        networkCount++;
634        return null;
635      } else {
636        hitCount++;
637        return new ValidationResult(e.v);
638      }
639    }
640  }
641
642  public void cacheValidation(CacheToken cacheToken, ValidationResult res, boolean persistent) {
643    if (cacheToken.key != null) {
644      synchronized (lock) {      
645        NamedCache nc = getNamedCache(cacheToken);
646        CacheEntry e = new CacheEntry();
647        e.request = cacheToken.request;
648        e.persistent = persistent;
649        e.v = new ValidationResult(res);
650        store(cacheToken, persistent, nc, e);
651      }    
652    }
653  }
654
655
656  // persistence
657
658  public void save() {
659
660  }
661
662  private <K extends Resource> void save(K resource, String title) {
663    if (folder == null)
664      return;
665
666    try {
667      OutputStreamWriter sw = new OutputStreamWriter(ManagedFileAccess.outStream(Utilities.path(folder, title + CACHE_FILE_EXTENSION)), "UTF-8");
668
669      JsonParser json = new JsonParser();
670      json.setOutputStyle(OutputStyle.PRETTY);
671
672      sw.write(json.composeString(resource).trim());
673      sw.close();
674    } catch (Exception e) {
675      System.out.println("error saving capability statement "+e.getMessage());
676    }
677  }
678
679  private void save(NamedCache nc) {
680    if (folder == null)
681      return;
682
683    try {
684      OutputStreamWriter sw = new OutputStreamWriter(ManagedFileAccess.outStream(Utilities.path(folder, nc.name+CACHE_FILE_EXTENSION)), "UTF-8");
685      sw.write(ENTRY_MARKER+"\r\n");
686      JsonParser json = new JsonParser();
687      json.setOutputStyle(OutputStyle.PRETTY);
688      for (CacheEntry ce : nc.list) {
689        sw.write(ce.request.trim());
690        sw.write(BREAK+"\r\n");
691        if (ce.e != null) {
692          sw.write("e: {\r\n");
693          if (ce.e.isFromServer())
694            sw.write("  \"from-server\" : true,\r\n");
695          if (ce.e.getValueset() != null)
696            sw.write("  \"valueSet\" : "+json.composeString(ce.e.getValueset()).trim()+",\r\n");
697          sw.write("  \"error\" : \""+Utilities.escapeJson(ce.e.getError()).trim()+"\"\r\n}\r\n");
698        } else if (ce.s != null) {
699          sw.write("s: {\r\n");
700          sw.write("  \"result\" : "+ce.s.result+"\r\n}\r\n");
701        } else {
702          sw.write("v: {\r\n");
703          boolean first = true;
704          if (ce.v.getDisplay() != null) {            
705            if (first) first = false; else sw.write(",\r\n");
706            sw.write("  \"display\" : \""+Utilities.escapeJson(ce.v.getDisplay()).trim()+"\"");
707          }
708          if (ce.v.getCode() != null) {
709            if (first) first = false; else sw.write(",\r\n");
710            sw.write("  \"code\" : \""+Utilities.escapeJson(ce.v.getCode()).trim()+"\"");
711          }
712          if (ce.v.getSystem() != null) {
713            if (first) first = false; else sw.write(",\r\n");
714            sw.write("  \"system\" : \""+Utilities.escapeJson(ce.v.getSystem()).trim()+"\"");
715          }
716          if (ce.v.getVersion() != null) {
717            if (first) first = false; else sw.write(",\r\n");
718            sw.write("  \"version\" : \""+Utilities.escapeJson(ce.v.getVersion()).trim()+"\"");
719          }
720          if (ce.v.getSeverity() != null) {
721            if (first) first = false; else sw.write(",\r\n");
722            sw.write("  \"severity\" : "+"\""+ce.v.getSeverity().toCode().trim()+"\""+"");
723          }
724          if (ce.v.getMessage() != null) {
725            if (first) first = false; else sw.write(",\r\n");
726            sw.write("  \"error\" : \""+Utilities.escapeJson(ce.v.getMessage()).trim()+"\"");
727          }
728          if (ce.v.getErrorClass() != null) {
729            if (first) first = false; else sw.write(",\r\n");
730            sw.write("  \"class\" : \""+Utilities.escapeJson(ce.v.getErrorClass().toString())+"\"");
731          }
732          if (ce.v.getDefinition() != null) {
733            if (first) first = false; else sw.write(",\r\n");
734            sw.write("  \"definition\" : \""+Utilities.escapeJson(ce.v.getDefinition()).trim()+"\"");
735          }
736          if (ce.v.getStatus() != null) {
737            if (first) first = false; else sw.write(",\r\n");
738            sw.write("  \"status\" : \""+Utilities.escapeJson(ce.v.getStatus()).trim()+"\"");
739          }
740          if (ce.v.getServer() != null) {
741            if (first) first = false; else sw.write(",\r\n");
742            sw.write("  \"server\" : \""+Utilities.escapeJson(ce.v.getServer()).trim()+"\"");
743          }
744          if (ce.v.isInactive()) {
745            if (first) first = false; else sw.write(",\r\n");
746            sw.write("  \"inactive\" : true");
747          }
748          if (ce.v.getUnknownSystems() != null) {
749            if (first) first = false; else sw.write(",\r\n");
750            sw.write("  \"unknown-systems\" : \""+Utilities.escapeJson(CommaSeparatedStringBuilder.join(",", ce.v.getUnknownSystems())).trim()+"\"");
751          }
752          if (ce.v.getIssues() != null) {
753            if (first) first = false; else sw.write(",\r\n");
754            OperationOutcome oo = new OperationOutcome();
755            oo.setIssue(ce.v.getIssues());
756            sw.write("  \"issues\" : "+json.composeString(oo).trim()+"\r\n");
757          }
758          sw.write("\r\n}\r\n");
759        }
760        sw.write(ENTRY_MARKER+"\r\n");
761      }      
762      sw.close();
763    } catch (Exception e) {
764      System.out.println("error saving "+nc.name+": "+e.getMessage());
765    }
766  }
767
768  private boolean isCapabilityCache(String fn) {
769    if (fn == null) {
770      return false;
771    }
772    return fn.startsWith(CAPABILITY_STATEMENT_TITLE) || fn.startsWith(TERMINOLOGY_CAPABILITIES_TITLE);
773  }
774
775  private void loadCapabilityCache(String fn) {
776    try {
777      String src = TextFile.fileToString(Utilities.path(folder, fn));
778      String serverId = Utilities.getFileNameForName(fn).replace(CACHE_FILE_EXTENSION, "");
779      serverId = serverId.substring(serverId.indexOf(".")+1);
780      serverId = serverId.substring(serverId.indexOf(".")+1);
781      String address = getServerForId(serverId);
782      if (address != null) {
783        JsonObject o = (JsonObject) new com.google.gson.JsonParser().parse(src);
784        Resource resource = new JsonParser().parse(o);
785
786        if (fn.startsWith(CAPABILITY_STATEMENT_TITLE)) {
787          this.capabilityStatementCache.put(address, (CapabilityStatement) resource);
788        } else if (fn.startsWith(TERMINOLOGY_CAPABILITIES_TITLE)) {
789          this.terminologyCapabilitiesCache.put(address, (TerminologyCapabilities) resource);
790        }
791      }
792    } catch (Exception e) {
793      e.printStackTrace();
794      throw new FHIRException("Error loading " + fn + ": " + e.getMessage(), e);
795    }
796  }
797
798  private String getServerForId(String serverId) {
799    for (String n : serverMap.keySet()) {
800      if (serverMap.get(n).equals(serverId)) {
801        return n;
802      }
803    }
804    return null;
805  }
806
807  private CacheEntry getCacheEntry(String request, String resultString) throws IOException {
808    CacheEntry ce = new CacheEntry();
809    ce.persistent = true;
810    ce.request = request;
811    char e = resultString.charAt(0);
812    resultString = resultString.substring(3);
813    JsonObject o = (JsonObject) new com.google.gson.JsonParser().parse(resultString);
814    String error = loadJS(o.get("error"));
815    if (e == 'e') {
816      if (o.has("valueSet"))
817        ce.e = new ValueSetExpansionOutcome((ValueSet) new JsonParser().parse(o.getAsJsonObject("valueSet")), error, TerminologyServiceErrorClass.UNKNOWN, o.has("from-server"));
818      else
819        ce.e = new ValueSetExpansionOutcome(error, TerminologyServiceErrorClass.UNKNOWN, o.has("from-server"));
820    } else if (e == 's') {
821      ce.s = new SubsumesResult(o.get("result").getAsBoolean());
822    } else {
823      String t = loadJS(o.get("severity"));
824      IssueSeverity severity = t == null ? null :  IssueSeverity.fromCode(t);
825      String display = loadJS(o.get("display"));
826      String code = loadJS(o.get("code"));
827      String system = loadJS(o.get("system"));
828      String version = loadJS(o.get("version"));
829      String definition = loadJS(o.get("definition"));
830      String server = loadJS(o.get("server"));
831      String status = loadJS(o.get("status"));
832      boolean inactive = "true".equals(loadJS(o.get("inactive")));
833      String unknownSystems = loadJS(o.get("unknown-systems"));
834      OperationOutcome oo = o.has("issues") ? (OperationOutcome) new JsonParser().parse(o.getAsJsonObject("issues")) : null;
835      t = loadJS(o.get("class")); 
836      TerminologyServiceErrorClass errorClass = t == null ? null : TerminologyServiceErrorClass.valueOf(t) ;
837      ce.v = new ValidationResult(severity, error, system, version, new ConceptDefinitionComponent().setDisplay(display).setDefinition(definition).setCode(code), display, null).setErrorClass(errorClass);
838      ce.v.setUnknownSystems(CommaSeparatedStringBuilder.toSet(unknownSystems));
839      ce.v.setServer(server);
840      ce.v.setStatus(inactive, status);
841      if (oo != null) {
842        ce.v.setIssues(oo.getIssue());
843      }
844    }
845    return ce;
846  }
847
848  private void loadNamedCache(String fn) {
849    int c = 0;
850    try {
851      String src = TextFile.fileToString(Utilities.path(folder, fn));
852      String title = fn.substring(0, fn.lastIndexOf("."));
853
854      NamedCache nc = new NamedCache();
855      nc.name = title;
856
857      if (src.startsWith("?"))
858        src = src.substring(1);
859      int i = src.indexOf(ENTRY_MARKER);
860      while (i > -1) {
861        c++;
862        String s = src.substring(0, i);
863        src = src.substring(i + ENTRY_MARKER.length() + 1);
864        i = src.indexOf(ENTRY_MARKER);
865        if (!Utilities.noString(s)) {
866          int j = s.indexOf(BREAK);
867          String request = s.substring(0, j);
868          String p = s.substring(j + BREAK.length() + 1).trim();
869
870          CacheEntry cacheEntry = getCacheEntry(request, p);
871
872          nc.map.put(String.valueOf(hashJson(cacheEntry.request)), cacheEntry);
873          nc.list.add(cacheEntry);
874        }
875        caches.put(nc.name, nc);
876      }        
877    } catch (Exception e) {
878      System.out.println("Error loading "+fn+": "+e.getMessage()+" entry "+c+" - ignoring it");
879      e.printStackTrace();
880    }
881  }
882
883  private void load() throws FHIRException, IOException {
884    IniFile ini = new IniFile(Utilities.path(folder, "servers.ini"));
885    if (ini.hasSection("servers")) {
886      for (String n : ini.getPropertyNames("servers")) {
887        serverMap.put(ini.getStringProperty("servers", n), n);
888      }
889    }
890
891    for (String fn : ManagedFileAccess.file(folder).list()) {
892      if (fn.endsWith(CACHE_FILE_EXTENSION) && !fn.equals("validation" + CACHE_FILE_EXTENSION)) {
893        try {
894          if (isCapabilityCache(fn)) {
895            loadCapabilityCache(fn);
896          } else {
897            loadNamedCache(fn);
898          }
899        } catch (FHIRException e) {
900          throw e;
901        }
902      }
903    }
904    try {
905      File f = ManagedFileAccess.file(Utilities.path(folder, "vs-externals.json"));
906      if (f.exists()) {
907        org.hl7.fhir.utilities.json.model.JsonObject json = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(f);
908        for (JsonProperty p : json.getProperties()) {
909          if (p.getValue().isJsonNull()) {
910            vsCache.put(p.getName(), null);
911          } else {
912            org.hl7.fhir.utilities.json.model.JsonObject j = p.getValue().asJsonObject();
913            vsCache.put(p.getName(), new SourcedValueSetEntry(j.asString("server"), j.asString("filename")));        
914          }
915        }
916      }
917    } catch (Exception e) {
918      System.out.println("Error loading vs external cache: "+e.getMessage());
919    }
920    try {
921      File f = ManagedFileAccess.file(Utilities.path(folder, "cs-externals.json"));
922      if (f.exists()) {
923        org.hl7.fhir.utilities.json.model.JsonObject json = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(f);
924        for (JsonProperty p : json.getProperties()) {
925          if (p.getValue().isJsonNull()) {
926            csCache.put(p.getName(), null);
927          } else {
928            org.hl7.fhir.utilities.json.model.JsonObject j = p.getValue().asJsonObject();
929            csCache.put(p.getName(), new SourcedCodeSystemEntry(j.asString("server"), j.asString("filename")));        
930          }
931        }
932      }
933    } catch (Exception e) {
934      System.out.println("Error loading vs external cache: "+e.getMessage());
935    }
936  }
937
938  private String loadJS(JsonElement e) {
939    if (e == null)
940      return null;
941    if (!(e instanceof JsonPrimitive))
942      return null;
943    String s = e.getAsString();
944    if ("".equals(s))
945      return null;
946    return s;
947  }
948
949  public String hashJson(String s) {
950    return String.valueOf(s.trim().hashCode());
951  }
952
953  // management
954
955  public String summary(ValueSet vs) {
956    if (vs == null)
957      return "null";
958
959    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
960    for (ConceptSetComponent cc : vs.getCompose().getInclude())
961      b.append("Include "+getIncSummary(cc));
962    for (ConceptSetComponent cc : vs.getCompose().getExclude())
963      b.append("Exclude "+getIncSummary(cc));
964    return b.toString();
965  }
966
967  private String getIncSummary(ConceptSetComponent cc) {
968    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
969    for (UriType vs : cc.getValueSet())
970      b.append(vs.asStringValue());
971    String vsd = b.length() > 0 ? " where the codes are in the value sets ("+b.toString()+")" : "";
972    String system = cc.getSystem();
973    if (cc.hasConcept())
974      return Integer.toString(cc.getConcept().size())+" codes from "+system+vsd;
975    if (cc.hasFilter()) {
976      String s = "";
977      for (ConceptSetFilterComponent f : cc.getFilter()) {
978        if (!Utilities.noString(s))
979          s = s + " & ";
980        s = s + f.getProperty()+" "+(f.hasOp() ? f.getOp().toCode() : "?")+" "+f.getValue();
981      }
982      return "from "+system+" where "+s+vsd;
983    }
984    return "All codes from "+system+vsd;
985  }
986
987  public String summary(Coding code) {
988    return code.getSystem()+"#"+code.getCode()+(code.hasDisplay() ? ": \""+code.getDisplay()+"\"" : "");
989  }
990
991  public String summary(CodeableConcept code) {
992    StringBuilder b = new StringBuilder();
993    b.append("{");
994    boolean first = true;
995    for (Coding c : code.getCoding()) {
996      if (first) first = false; else b.append(",");
997      b.append(summary(c));
998    }
999    b.append("}: \"");
1000    b.append(code.getText());
1001    b.append("\"");
1002    return b.toString();
1003  }
1004
1005  public void removeCS(String url) {
1006    synchronized (lock) {
1007      String name = getSystemNameKeyGenerator().getNameForSystem(url);
1008      if (caches.containsKey(name)) {
1009        caches.remove(name);
1010      }
1011    }   
1012  }
1013
1014  public String getFolder() {
1015    return folder;
1016  }
1017
1018  public Map<String, String> servers() {
1019    Map<String, String> servers = new HashMap<>();
1020//    servers.put("http://local.fhir.org/r2", "tx.fhir.org");
1021//    servers.put("http://local.fhir.org/r3", "tx.fhir.org");
1022//    servers.put("http://local.fhir.org/r4", "tx.fhir.org");
1023//    servers.put("http://local.fhir.org/r5", "tx.fhir.org");
1024//
1025//    servers.put("http://tx-dev.fhir.org/r2", "tx.fhir.org");
1026//    servers.put("http://tx-dev.fhir.org/r3", "tx.fhir.org");
1027//    servers.put("http://tx-dev.fhir.org/r4", "tx.fhir.org");
1028//    servers.put("http://tx-dev.fhir.org/r5", "tx.fhir.org");
1029
1030    servers.put("http://tx.fhir.org/r2", "tx.fhir.org");
1031    servers.put("http://tx.fhir.org/r3", "tx.fhir.org");
1032    servers.put("http://tx.fhir.org/r4", "tx.fhir.org");
1033    servers.put("http://tx.fhir.org/r5", "tx.fhir.org");
1034
1035    return servers;
1036  }
1037
1038  public boolean hasValueSet(String canonical) {
1039    return vsCache.containsKey(canonical);
1040  }
1041
1042  public boolean hasCodeSystem(String canonical) {
1043    return csCache.containsKey(canonical);
1044  }
1045
1046  public SourcedValueSet getValueSet(String canonical) {
1047    SourcedValueSetEntry sp = vsCache.get(canonical);
1048    if (sp == null || folder == null) {
1049      return null;
1050    } else {
1051      try {
1052        return new SourcedValueSet(sp.getServer(), sp.getFilename() == null ? null : (ValueSet) new JsonParser().parse(ManagedFileAccess.inStream(Utilities.path(folder, sp.getFilename()))));
1053      } catch (Exception e) {
1054        return null;
1055      }
1056    }
1057  }
1058
1059  public SourcedCodeSystem getCodeSystem(String canonical) {
1060    SourcedCodeSystemEntry sp = csCache.get(canonical);
1061    if (sp == null || folder == null) {
1062      return null;
1063    } else {
1064      try {
1065        return new SourcedCodeSystem(sp.getServer(), sp.getFilename() == null ? null : (CodeSystem) new JsonParser().parse(ManagedFileAccess.inStream(Utilities.path(folder, sp.getFilename()))));
1066      } catch (Exception e) {
1067        return null;
1068      }
1069    }
1070  }
1071
1072  public void cacheValueSet(String canonical, SourcedValueSet svs) {
1073    if (canonical == null) {
1074      return;
1075    }
1076    try {
1077      if (svs == null) {
1078        vsCache.put(canonical, null);
1079      } else {
1080        String uuid = Utilities.makeUuidLC();
1081        String fn = "vs-"+uuid+".json";
1082        if (folder != null) {
1083          new JsonParser().compose(ManagedFileAccess.outStream(Utilities.path(folder, fn)), svs.getVs());
1084        }
1085        vsCache.put(canonical, new SourcedValueSetEntry(svs.getServer(), fn));
1086      }    
1087      org.hl7.fhir.utilities.json.model.JsonObject j = new org.hl7.fhir.utilities.json.model.JsonObject();
1088      for (String k : vsCache.keySet()) {
1089        SourcedValueSetEntry sve = vsCache.get(k);
1090        if (sve == null) {
1091          j.add(k, new JsonNull());
1092        } else {
1093          org.hl7.fhir.utilities.json.model.JsonObject e = new org.hl7.fhir.utilities.json.model.JsonObject();
1094          e.set("server", sve.getServer());
1095          if (sve.getFilename() != null) {
1096            e.set("filename", sve.getFilename());
1097          }
1098          j.add(k, e);
1099        }
1100      }
1101      if (folder != null) {
1102        org.hl7.fhir.utilities.json.parser.JsonParser.compose(j, ManagedFileAccess.file(Utilities.path(folder, "vs-externals.json")), true);
1103      }
1104    } catch (Exception e) {
1105      e.printStackTrace();
1106    }
1107  }
1108
1109  public void cacheCodeSystem(String canonical, SourcedCodeSystem scs) {
1110    if (canonical == null) {
1111      return;
1112    }
1113    try {
1114      if (scs == null) {
1115        csCache.put(canonical, null);
1116      } else {
1117        String uuid = Utilities.makeUuidLC();
1118        String fn = "cs-"+uuid+".json";
1119        if (folder != null) {
1120          new JsonParser().compose(ManagedFileAccess.outStream(Utilities.path(folder, fn)), scs.getCs());
1121        }
1122        csCache.put(canonical, new SourcedCodeSystemEntry(scs.getServer(), fn));
1123      }    
1124      org.hl7.fhir.utilities.json.model.JsonObject j = new org.hl7.fhir.utilities.json.model.JsonObject();
1125      for (String k : csCache.keySet()) {
1126        SourcedCodeSystemEntry sve = csCache.get(k);
1127        if (sve == null) {
1128          j.add(k, new JsonNull());
1129        } else {
1130          org.hl7.fhir.utilities.json.model.JsonObject e = new org.hl7.fhir.utilities.json.model.JsonObject();
1131          e.set("server", sve.getServer());
1132          if (sve.getFilename() != null) {
1133            e.set("filename", sve.getFilename());
1134          }
1135          j.add(k, e);
1136        }
1137      }
1138      if (folder != null) {
1139        org.hl7.fhir.utilities.json.parser.JsonParser.compose(j, ManagedFileAccess.file(Utilities.path(folder, "cs-externals.json")), true);
1140      }
1141    } catch (Exception e) {
1142      e.printStackTrace();
1143    }
1144  }
1145
1146  public CacheToken generateSubsumesToken(ValidationOptions options, Coding parent, Coding child, Parameters expParameters) {
1147    try {
1148      CacheToken ct = new CacheToken();
1149      if (parent.hasSystem()) {
1150        ct.setName(parent.getSystem());
1151      }
1152      if (child.hasSystem()) {
1153        ct.setName(child.getSystem());
1154      }
1155      ct.hasVersion = parent.hasVersion() || child.hasVersion();
1156      JsonParser json = new JsonParser();
1157      json.setOutputStyle(OutputStyle.PRETTY);
1158      String expJS = json.composeString(expParameters);
1159      ct.request = "{\"op\": \"subsumes\", \"parent\" : "+json.composeString(parent, "code")+", \"child\" :"+json.composeString(child, "code")+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}";
1160      ct.key = String.valueOf(hashJson(ct.request));
1161      return ct;
1162    } catch (IOException e) {
1163      throw new Error(e);
1164    }
1165  }
1166
1167  public Boolean getSubsumes(CacheToken cacheToken) {
1168   if (cacheToken.key == null) {
1169     return null;
1170   }
1171   synchronized (lock) {
1172     requestCount++;
1173     NamedCache nc = getNamedCache(cacheToken);
1174     CacheEntry e = nc.map.get(cacheToken.key);
1175     if (e == null) {
1176       networkCount++;
1177       return null;
1178     } else {
1179       hitCount++;
1180       return e.s.result;
1181     }
1182   }
1183   
1184  }
1185
1186  public void cacheSubsumes(CacheToken cacheToken, Boolean b, boolean persistent) {
1187    if (cacheToken.key != null) {
1188      synchronized (lock) {      
1189        NamedCache nc = getNamedCache(cacheToken);
1190        CacheEntry e = new CacheEntry();
1191        e.request = cacheToken.request;
1192        e.persistent = persistent;
1193        e.s = new SubsumesResult(b);
1194        store(cacheToken, persistent, nc, e);
1195      }    
1196    }
1197  }
1198
1199
1200}