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