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