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