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