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