001package org.hl7.fhir.r5.context;
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.FileOutputStream;
037import java.io.IOException;
038import java.io.OutputStreamWriter;
039import java.util.*;
040
041import lombok.Getter;
042import lombok.Setter;
043import lombok.experimental.Accessors;
044import org.apache.commons.lang3.StringUtils;
045import org.hl7.fhir.exceptions.FHIRException;
046import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult;
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.OperationOutcome.OperationOutcomeIssueComponent;
052import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent;
053import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent;
054import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent;
055import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome;
056import org.hl7.fhir.r5.terminologies.utilities.TerminologyServiceErrorClass;
057import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
058import org.hl7.fhir.utilities.TextFile;
059import org.hl7.fhir.utilities.Utilities;
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 */
077public class TerminologyCache {
078  
079  public static final boolean TRANSIENT = false;
080  public static final boolean PERMANENT = true;
081  private static final String NAME_FOR_NO_SYSTEM = "all-systems";
082  private static final String ENTRY_MARKER = "-------------------------------------------------------------------------------------";
083  private static final String BREAK = "####";
084  private static final String CACHE_FILE_EXTENSION = ".cache";
085  private static final String CAPABILITY_STATEMENT_TITLE = ".capabilityStatement";
086  private static final String TERMINOLOGY_CAPABILITIES_TITLE = ".terminologyCapabilities";
087
088
089  private SystemNameKeyGenerator systemNameKeyGenerator = new SystemNameKeyGenerator();
090
091  public class CacheToken {
092    @Getter
093    private String name;
094    private String key;
095    @Getter
096    private String request;
097    @Accessors(fluent = true)
098    @Getter
099    private boolean hasVersion;
100
101    public void setName(String n) {
102      String systemName = getSystemNameKeyGenerator().getNameForSystem(n);
103      if (name == null)
104        name = systemName;
105      else if (!systemName.equals(name))
106        name = NAME_FOR_NO_SYSTEM;
107    }
108  }
109
110  protected SystemNameKeyGenerator getSystemNameKeyGenerator() {
111    return systemNameKeyGenerator;
112  }
113  public class SystemNameKeyGenerator {
114    public static final String SNOMED_SCT_CODESYSTEM_URL = "http://snomed.info/sct";
115    public static final String RXNORM_CODESYSTEM_URL = "http://www.nlm.nih.gov/research/umls/rxnorm";
116    public static final String LOINC_CODESYSTEM_URL = "http://loinc.org";
117    public static final String UCUM_CODESYSTEM_URL = "http://unitsofmeasure.org";
118
119    public static final String HL7_TERMINOLOGY_CODESYSTEM_BASE_URL = "http://terminology.hl7.org/CodeSystem/";
120    public static final String HL7_SID_CODESYSTEM_BASE_URL = "http://hl7.org/fhir/sid/";
121    public static final String HL7_FHIR_CODESYSTEM_BASE_URL = "http://hl7.org/fhir/";
122
123    public static final String ISO_CODESYSTEM_URN = "urn:iso:std:iso:";
124    public static final String LANG_CODESYSTEM_URN = "urn:ietf:bcp:47";
125    public static final String MIMETYPES_CODESYSTEM_URN = "urn:ietf:bcp:13";
126
127    public static final String _11073_CODESYSTEM_URN = "urn:iso:std:iso:11073:10101";
128    public static final String DICOM_CODESYSTEM_URL = "http://dicom.nema.org/resources/ontology/DCM";
129
130    public String getNameForSystem(String system) {
131      final int lastPipe = system.lastIndexOf('|');
132      final String systemBaseName = lastPipe == -1 ? system : system.substring(0,lastPipe);
133      final String systemVersion = lastPipe == -1 ? null : system.substring(lastPipe + 1);
134
135      if (systemBaseName.equals(SNOMED_SCT_CODESYSTEM_URL))
136        return getVersionedSystem("snomed", systemVersion);
137      if (systemBaseName.equals(RXNORM_CODESYSTEM_URL))
138        return getVersionedSystem("rxnorm", systemVersion);
139      if (systemBaseName.equals(LOINC_CODESYSTEM_URL))
140        return getVersionedSystem("loinc", systemVersion);
141      if (systemBaseName.equals(UCUM_CODESYSTEM_URL))
142        return getVersionedSystem("ucum", systemVersion);
143      if (systemBaseName.startsWith(HL7_SID_CODESYSTEM_BASE_URL))
144        return getVersionedSystem(normalizeBaseURL(HL7_SID_CODESYSTEM_BASE_URL, systemBaseName), systemVersion);
145      if (systemBaseName.equals(_11073_CODESYSTEM_URN))
146        return getVersionedSystem("11073", systemVersion);
147      if (systemBaseName.startsWith(ISO_CODESYSTEM_URN))
148        return getVersionedSystem("iso"+systemBaseName.substring(ISO_CODESYSTEM_URN.length()).replace(":", ""), systemVersion);
149      if (systemBaseName.startsWith(HL7_TERMINOLOGY_CODESYSTEM_BASE_URL))
150        return getVersionedSystem(normalizeBaseURL(HL7_TERMINOLOGY_CODESYSTEM_BASE_URL, systemBaseName), systemVersion);
151      if (systemBaseName.startsWith(HL7_FHIR_CODESYSTEM_BASE_URL))
152        return getVersionedSystem(normalizeBaseURL(HL7_FHIR_CODESYSTEM_BASE_URL, systemBaseName), systemVersion);
153      if (systemBaseName.equals(LANG_CODESYSTEM_URN))
154        return getVersionedSystem("lang", systemVersion);
155      if (systemBaseName.equals(MIMETYPES_CODESYSTEM_URN))
156        return getVersionedSystem("mimetypes", systemVersion);
157      if (systemBaseName.equals(DICOM_CODESYSTEM_URL))
158        return getVersionedSystem("dicom", systemVersion);
159      return getVersionedSystem(systemBaseName.replace("/", "_").replace(":", "_").replace("?", "X").replace("#", "X"), systemVersion);
160    }
161
162    public String normalizeBaseURL(String baseUrl, String fullUrl) {
163      return fullUrl.substring(baseUrl.length()).replace("/", "");
164    }
165
166    public String getVersionedSystem(String baseSystem, String version) {
167      if (version != null) {
168        return baseSystem + "_" + version;
169      }
170      return baseSystem;
171    }
172  }
173
174
175  private class CacheEntry {
176    private String request;
177    private boolean persistent;
178    private ValidationResult v;
179    private ValueSetExpansionOutcome e;
180  }
181
182  private class NamedCache {
183    private String name; 
184    private List<CacheEntry> list = new ArrayList<CacheEntry>(); // persistent entries
185    private Map<String, CacheEntry> map = new HashMap<String, CacheEntry>();
186  }
187
188
189  private Object lock;
190  private String folder;
191  @Getter private int requestCount;
192  @Getter private int hitCount;
193  @Getter private int networkCount;
194  private CapabilityStatement capabilityStatementCache = null;
195  private TerminologyCapabilities terminologyCapabilitiesCache = null;
196  private Map<String, NamedCache> caches = new HashMap<String, NamedCache>();
197  @Getter @Setter private static boolean noCaching;
198
199  @Getter @Setter private static boolean cacheErrors;
200
201
202  // use lock from the context
203  public TerminologyCache(Object lock, String folder) throws FileNotFoundException, IOException, FHIRException {
204    super();
205    this.lock = lock;
206    this.folder = folder;
207    requestCount = 0;
208    hitCount = 0;
209    networkCount = 0;
210
211    if (folder != null) {
212      load();
213    }
214  }
215
216  public boolean hasCapabilityStatement() {
217    return capabilityStatementCache != null;
218  }
219
220  public CapabilityStatement getCapabilityStatement() {
221    return capabilityStatementCache;
222  }
223
224  public void cacheCapabilityStatement(CapabilityStatement capabilityStatement) {
225    if (noCaching) {
226      return;
227    } 
228    this.capabilityStatementCache = capabilityStatement;
229    save(capabilityStatementCache, CAPABILITY_STATEMENT_TITLE);
230  }
231
232
233  public boolean hasTerminologyCapabilities() {
234    return terminologyCapabilitiesCache != null;
235  }
236
237  public TerminologyCapabilities getTerminologyCapabilities() {
238    return terminologyCapabilitiesCache;
239  }
240
241  public void cacheTerminologyCapabilities(TerminologyCapabilities terminologyCapabilities) {
242    if (noCaching) {
243      return;
244    }
245    this.terminologyCapabilitiesCache = terminologyCapabilities;
246    save(terminologyCapabilitiesCache, TERMINOLOGY_CAPABILITIES_TITLE);
247  }
248
249
250  public void clear() {
251    caches.clear();
252  }
253
254  public CacheToken generateValidationToken(ValidationOptions options, Coding code, ValueSet vs, Parameters expParameters) {
255    try {
256      CacheToken ct = new CacheToken();
257      if (code.hasSystem()) {
258        ct.setName(code.getSystem());
259        ct.hasVersion = code.hasVersion();
260      }
261      else
262        ct.name = NAME_FOR_NO_SYSTEM;
263      nameCacheToken(vs, ct);
264      JsonParser json = new JsonParser();
265      json.setOutputStyle(OutputStyle.PRETTY);
266      String expJS = json.composeString(expParameters);
267
268      if (vs != null && vs.hasUrl() && vs.hasVersion()) {
269        ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+", \"url\": \""+Utilities.escapeJson(vs.getUrl())
270        +"\", \"version\": \""+Utilities.escapeJson(vs.getVersion())+"\""+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}\r\n";      
271      } else {
272        ValueSet vsc = getVSEssense(vs);
273        ct.request = "{\"code\" : "+json.composeString(code, "code")+", \"valueSet\" :"+(vsc == null ? "null" : extracted(json, vsc))+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}";
274      }
275      ct.key = String.valueOf(hashJson(ct.request));
276      return ct;
277    } catch (IOException e) {
278      throw new Error(e);
279    }
280  }
281
282  public CacheToken generateValidationToken(ValidationOptions options, Coding code, String vsUrl, Parameters expParameters) {
283    try {
284      CacheToken ct = new CacheToken();
285      if (code.hasSystem()) {
286        ct.setName(code.getSystem());
287        ct.hasVersion = code.hasVersion();
288      }
289      else
290        ct.name = NAME_FOR_NO_SYSTEM;
291      ct.setName(vsUrl);
292      JsonParser json = new JsonParser();
293      json.setOutputStyle(OutputStyle.PRETTY);
294      String expJS = json.composeString(expParameters);
295
296      ct.request = "{\"code\" : "+json.composeString(code, "code")+", \"valueSet\" :"+(vsUrl == null ? "null" : vsUrl)+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}";
297      ct.key = String.valueOf(hashJson(ct.request));
298      return ct;
299    } catch (IOException e) {
300      throw new Error(e);
301    }
302  }
303
304  public String extracted(JsonParser json, ValueSet vsc) throws IOException {
305    String s = null;
306    if (vsc.getExpansion().getContains().size() > 1000 || vsc.getCompose().getIncludeFirstRep().getConcept().size() > 1000) {      
307      s =  vsc.getUrl();
308    } else {
309      s = json.composeString(vsc);
310    }
311    return s;
312  }
313
314  public CacheToken generateValidationToken(ValidationOptions options, CodeableConcept code, ValueSet vs, Parameters expParameters) {
315    try {
316      CacheToken ct = new CacheToken();
317      for (Coding c : code.getCoding()) {
318        if (c.hasSystem()) {
319          ct.setName(c.getSystem());
320          ct.hasVersion = c.hasVersion();
321        }
322      }
323      nameCacheToken(vs, ct);
324      JsonParser json = new JsonParser();
325      json.setOutputStyle(OutputStyle.PRETTY);
326      String expJS = json.composeString(expParameters);
327      if (vs != null && vs.hasUrl() && vs.hasVersion()) {
328        ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+", \"url\": \""+Utilities.escapeJson(vs.getUrl())+
329            "\", \"version\": \""+Utilities.escapeJson(vs.getVersion())+"\""+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}\r\n";      
330      } else {
331        ValueSet vsc = getVSEssense(vs);
332        ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+", \"valueSet\" :"+extracted(json, vsc)+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}";
333      }
334      ct.key = String.valueOf(hashJson(ct.request));
335      return ct;
336    } catch (IOException e) {
337      throw new Error(e);
338    }
339  }
340
341  public ValueSet getVSEssense(ValueSet vs) {
342    if (vs == null)
343      return null;
344    ValueSet vsc = new ValueSet();
345    vsc.setCompose(vs.getCompose());
346    if (vs.hasExpansion()) {
347      vsc.getExpansion().getParameter().addAll(vs.getExpansion().getParameter());
348      vsc.getExpansion().getContains().addAll(vs.getExpansion().getContains());
349    }
350    return vsc;
351  }
352
353  public CacheToken generateExpandToken(ValueSet vs, boolean hierarchical) {
354    CacheToken ct = new CacheToken();
355    nameCacheToken(vs, ct);
356    if (vs.hasUrl() && vs.hasVersion()) {
357      ct.request = "{\"hierarchical\" : "+(hierarchical ? "true" : "false")+", \"url\": \""+Utilities.escapeJson(vs.getUrl())+"\", \"version\": \""+Utilities.escapeJson(vs.getVersion())+"\"}\r\n";      
358    } else {
359      ValueSet vsc = getVSEssense(vs);
360      JsonParser json = new JsonParser();
361      json.setOutputStyle(OutputStyle.PRETTY);
362      try {
363        ct.request = "{\"hierarchical\" : "+(hierarchical ? "true" : "false")+", \"valueSet\" :"+extracted(json, vsc)+"}\r\n";
364      } catch (IOException e) {
365        throw new Error(e);
366      }
367    }
368    ct.key = String.valueOf(hashJson(ct.request));
369    return ct;
370  }
371
372  public void nameCacheToken(ValueSet vs, CacheToken ct) {
373    if (vs != null) {
374      for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
375        if (inc.hasSystem()) {
376          ct.setName(inc.getSystem());
377          ct.hasVersion = inc.hasVersion();
378        }
379      }
380      for (ConceptSetComponent inc : vs.getCompose().getExclude()) {
381        if (inc.hasSystem()) {
382          ct.setName(inc.getSystem());
383          ct.hasVersion = inc.hasVersion();
384        }
385      }
386      for (ValueSetExpansionContainsComponent inc : vs.getExpansion().getContains()) {
387        if (inc.hasSystem()) {
388          ct.setName(inc.getSystem());
389          ct.hasVersion = inc.hasVersion();
390        }
391      }
392    }
393  }
394
395  private String normalizeSystemPath(String path) {
396    return path.replace("/", "").replace('|','X');
397  }
398
399
400
401  public NamedCache getNamedCache(CacheToken cacheToken) {
402
403    final String cacheName = cacheToken.name == null ? "null" : cacheToken.name;
404
405    NamedCache nc = caches.get(cacheName);
406
407    if (nc == null) {
408      nc = new NamedCache();
409      nc.name = cacheName;
410      caches.put(nc.name, nc);
411    }
412    return nc;
413  }
414
415  public ValueSetExpansionOutcome getExpansion(CacheToken cacheToken) {
416    synchronized (lock) {
417      NamedCache nc = getNamedCache(cacheToken);
418      CacheEntry e = nc.map.get(cacheToken.key);
419      if (e == null)
420        return null;
421      else
422        return e.e;
423    }
424  }
425
426  public void cacheExpansion(CacheToken cacheToken, ValueSetExpansionOutcome res, boolean persistent) {
427    synchronized (lock) {      
428      NamedCache nc = getNamedCache(cacheToken);
429      CacheEntry e = new CacheEntry();
430      e.request = cacheToken.request;
431      e.persistent = persistent;
432      e.e = res;
433      store(cacheToken, persistent, nc, e);
434    }    
435  }
436
437  public void store(CacheToken cacheToken, boolean persistent, NamedCache nc, CacheEntry e) {
438    if (noCaching) {
439      return;
440    }
441
442    if ( !cacheErrors &&
443        ( e.v!= null
444        && e.v.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED
445        && !cacheToken.hasVersion)) {
446      return;
447    }
448
449    boolean n = nc.map.containsKey(cacheToken.key);
450    nc.map.put(cacheToken.key, e);
451    if (persistent) {
452      if (n) {
453        for (int i = nc.list.size()- 1; i>= 0; i--) {
454          if (nc.list.get(i).request.equals(e.request)) {
455            nc.list.remove(i);
456          }
457        }
458      }
459      nc.list.add(e);
460      save(nc);  
461    }
462  }
463
464  public ValidationResult getValidation(CacheToken cacheToken) {
465    if (cacheToken.key == null) {
466      return null;
467    }
468    synchronized (lock) {
469      requestCount++;
470      NamedCache nc = getNamedCache(cacheToken);
471      CacheEntry e = nc.map.get(cacheToken.key);
472      if (e == null) {
473        networkCount++;
474        return null;
475      } else {
476        hitCount++;
477        return e.v;
478      }
479    }
480  }
481
482  public void cacheValidation(CacheToken cacheToken, ValidationResult res, boolean persistent) {
483    if (cacheToken.key != null) {
484      synchronized (lock) {      
485        NamedCache nc = getNamedCache(cacheToken);
486        CacheEntry e = new CacheEntry();
487        e.request = cacheToken.request;
488        e.persistent = persistent;
489        e.v = res;
490        store(cacheToken, persistent, nc, e);
491      }    
492    }
493  }
494
495
496  // persistence
497
498  public void save() {
499
500  }
501
502  private <K extends Resource> void save(K resource, String title) {
503    if (folder == null)
504      return;
505
506    try {
507      OutputStreamWriter sw = new OutputStreamWriter(new FileOutputStream(Utilities.path(folder, title + CACHE_FILE_EXTENSION)), "UTF-8");
508
509      JsonParser json = new JsonParser();
510      json.setOutputStyle(OutputStyle.PRETTY);
511
512      sw.write(json.composeString(resource).trim());
513      sw.close();
514    } catch (Exception e) {
515      System.out.println("error saving capability statement "+e.getMessage());
516    }
517  }
518
519  private void save(NamedCache nc) {
520    if (folder == null)
521      return;
522
523    try {
524      OutputStreamWriter sw = new OutputStreamWriter(new FileOutputStream(Utilities.path(folder, nc.name+CACHE_FILE_EXTENSION)), "UTF-8");
525      sw.write(ENTRY_MARKER+"\r\n");
526      JsonParser json = new JsonParser();
527      json.setOutputStyle(OutputStyle.PRETTY);
528      for (CacheEntry ce : nc.list) {
529        sw.write(ce.request.trim());
530        sw.write(BREAK+"\r\n");
531        if (ce.e != null) {
532          sw.write("e: {\r\n");
533          if (ce.e.getValueset() != null)
534            sw.write("  \"valueSet\" : "+json.composeString(ce.e.getValueset()).trim()+",\r\n");
535          sw.write("  \"error\" : \""+Utilities.escapeJson(ce.e.getError()).trim()+"\"\r\n}\r\n");
536        } else {
537          sw.write("v: {\r\n");
538          boolean first = true;
539          if (ce.v.getDisplay() != null) {            
540            if (first) first = false; else sw.write(",\r\n");
541            sw.write("  \"display\" : \""+Utilities.escapeJson(ce.v.getDisplay()).trim()+"\"");
542          }
543          if (ce.v.getCode() != null) {
544            if (first) first = false; else sw.write(",\r\n");
545            sw.write("  \"code\" : \""+Utilities.escapeJson(ce.v.getCode()).trim()+"\"");
546          }
547          if (ce.v.getSystem() != null) {
548            if (first) first = false; else sw.write(",\r\n");
549            sw.write("  \"system\" : \""+Utilities.escapeJson(ce.v.getSystem()).trim()+"\"");
550          }
551          if (ce.v.getVersion() != null) {
552            if (first) first = false; else sw.write(",\r\n");
553            sw.write("  \"version\" : \""+Utilities.escapeJson(ce.v.getVersion()).trim()+"\"");
554          }
555          if (ce.v.getSeverity() != null) {
556            if (first) first = false; else sw.write(",\r\n");
557            sw.write("  \"severity\" : "+"\""+ce.v.getSeverity().toCode().trim()+"\""+"");
558          }
559          if (ce.v.getMessage() != null) {
560            if (first) first = false; else sw.write(",\r\n");
561            sw.write("  \"error\" : \""+Utilities.escapeJson(ce.v.getMessage()).trim()+"\"");
562          }
563          if (ce.v.getErrorClass() != null) {
564            if (first) first = false; else sw.write(",\r\n");
565            sw.write("  \"class\" : \""+Utilities.escapeJson(ce.v.getErrorClass().toString())+"\"");
566          }
567          if (ce.v.getDefinition() != null) {
568            if (first) first = false; else sw.write(",\r\n");
569            sw.write("  \"definition\" : \""+Utilities.escapeJson(ce.v.getDefinition()).trim()+"\"");
570          }
571          if (ce.v.getUnknownSystems() != null) {
572            if (first) first = false; else sw.write(",\r\n");
573            sw.write("  \"unknown-systems\" : \""+Utilities.escapeJson(CommaSeparatedStringBuilder.join(",", ce.v.getUnknownSystems())).trim()+"\"");
574          }
575          if (ce.v.getIssues() != null) {
576            if (first) first = false; else sw.write(",\r\n");
577            OperationOutcome oo = new OperationOutcome();
578            oo.setIssue(ce.v.getIssues());
579            sw.write("  \"issues\" : "+json.composeString(oo).trim()+"\r\n");
580          }
581          sw.write("\r\n}\r\n");
582        }
583        sw.write(ENTRY_MARKER+"\r\n");
584      }      
585      sw.close();
586    } catch (Exception e) {
587      System.out.println("error saving "+nc.name+": "+e.getMessage());
588    }
589  }
590
591  private boolean isCapabilityCache(String fn) {
592    if (fn == null) {
593      return false;
594    }
595    return fn.startsWith(CAPABILITY_STATEMENT_TITLE) || fn.startsWith(TERMINOLOGY_CAPABILITIES_TITLE);
596  }
597
598  private void loadCapabilityCache(String fn) {
599    try {
600      String src = TextFile.fileToString(Utilities.path(folder, fn));
601
602      JsonObject o = (JsonObject) new com.google.gson.JsonParser().parse(src);
603      Resource resource = new JsonParser().parse(o);
604
605      if (fn.startsWith(CAPABILITY_STATEMENT_TITLE)) {
606        this.capabilityStatementCache = (CapabilityStatement) resource;
607      } else if (fn.startsWith(TERMINOLOGY_CAPABILITIES_TITLE)) {
608        this.terminologyCapabilitiesCache = (TerminologyCapabilities) resource;
609      }
610    } catch (Exception e) {
611      e.printStackTrace();
612      throw new FHIRException("Error loading " + fn + ": " + e.getMessage(), e);
613    }
614  }
615
616  private CacheEntry getCacheEntry(String request, String resultString) throws IOException {
617    CacheEntry ce = new CacheEntry();
618    ce.persistent = true;
619    ce.request = request;
620    boolean e = resultString.charAt(0) == 'e';
621    resultString = resultString.substring(3);
622    JsonObject o = (JsonObject) new com.google.gson.JsonParser().parse(resultString);
623    String error = loadJS(o.get("error"));
624    if (e) {
625      if (o.has("valueSet"))
626        ce.e = new ValueSetExpansionOutcome((ValueSet) new JsonParser().parse(o.getAsJsonObject("valueSet")), error, TerminologyServiceErrorClass.UNKNOWN);
627      else
628        ce.e = new ValueSetExpansionOutcome(error, TerminologyServiceErrorClass.UNKNOWN);
629    } else {
630      String t = loadJS(o.get("severity"));
631      IssueSeverity severity = t == null ? null :  IssueSeverity.fromCode(t);
632      String display = loadJS(o.get("display"));
633      String code = loadJS(o.get("code"));
634      String system = loadJS(o.get("system"));
635      String version = loadJS(o.get("version"));
636      String definition = loadJS(o.get("definition"));
637      String unknownSystems = loadJS(o.get("unknown-systems"));
638      OperationOutcome oo = o.has("issues") ? (OperationOutcome) new JsonParser().parse(o.getAsJsonObject("issues")) : null;
639      t = loadJS(o.get("class")); 
640      TerminologyServiceErrorClass errorClass = t == null ? null : TerminologyServiceErrorClass.valueOf(t) ;
641      ce.v = new ValidationResult(severity, error, system, version, new ConceptDefinitionComponent().setDisplay(display).setDefinition(definition).setCode(code), display, null).setErrorClass(errorClass);
642      ce.v.setUnknownSystems(CommaSeparatedStringBuilder.toSet(unknownSystems));
643      if (oo != null) {
644        ce.v.setIssues(oo.getIssue());
645      }
646    }
647    return ce;
648  }
649
650  private void loadNamedCache(String fn) {
651    int c = 0;
652    try {
653      String src = TextFile.fileToString(Utilities.path(folder, fn));
654      String title = fn.substring(0, fn.lastIndexOf("."));
655
656      NamedCache nc = new NamedCache();
657      nc.name = title;
658
659      if (src.startsWith("?"))
660        src = src.substring(1);
661      int i = src.indexOf(ENTRY_MARKER);
662      while (i > -1) {
663        c++;
664        String s = src.substring(0, i);
665        src = src.substring(i + ENTRY_MARKER.length() + 1);
666        i = src.indexOf(ENTRY_MARKER);
667        if (!Utilities.noString(s)) {
668          int j = s.indexOf(BREAK);
669          String request = s.substring(0, j);
670          String p = s.substring(j + BREAK.length() + 1).trim();
671
672          CacheEntry cacheEntry = getCacheEntry(request, p);
673
674          nc.map.put(String.valueOf(hashJson(cacheEntry.request)), cacheEntry);
675          nc.list.add(cacheEntry);
676        }
677        caches.put(nc.name, nc);
678      }        
679    } catch (Exception e) {
680      System.out.println("Error loading "+fn+": "+e.getMessage()+" entry "+c+" - ignoring it");
681      e.printStackTrace();
682    }
683  }
684
685  private void load() throws FHIRException {
686    for (String fn : new File(folder).list()) {
687      if (fn.endsWith(CACHE_FILE_EXTENSION) && !fn.equals("validation" + CACHE_FILE_EXTENSION)) {
688        try {
689          if (isCapabilityCache(fn)) {
690            loadCapabilityCache(fn);
691          } else {
692            loadNamedCache(fn);
693          }
694        } catch (FHIRException e) {
695          throw e;
696        }
697      }
698    }
699  }
700
701  private String loadJS(JsonElement e) {
702    if (e == null)
703      return null;
704    if (!(e instanceof JsonPrimitive))
705      return null;
706    String s = e.getAsString();
707    if ("".equals(s))
708      return null;
709    return s;
710  }
711
712  protected String hashJson(String s) {
713    return String.valueOf(s.trim().hashCode());
714  }
715
716  // management
717
718  public String summary(ValueSet vs) {
719    if (vs == null)
720      return "null";
721
722    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
723    for (ConceptSetComponent cc : vs.getCompose().getInclude())
724      b.append("Include "+getIncSummary(cc));
725    for (ConceptSetComponent cc : vs.getCompose().getExclude())
726      b.append("Exclude "+getIncSummary(cc));
727    return b.toString();
728  }
729
730  private String getIncSummary(ConceptSetComponent cc) {
731    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
732    for (UriType vs : cc.getValueSet())
733      b.append(vs.asStringValue());
734    String vsd = b.length() > 0 ? " where the codes are in the value sets ("+b.toString()+")" : "";
735    String system = cc.getSystem();
736    if (cc.hasConcept())
737      return Integer.toString(cc.getConcept().size())+" codes from "+system+vsd;
738    if (cc.hasFilter()) {
739      String s = "";
740      for (ConceptSetFilterComponent f : cc.getFilter()) {
741        if (!Utilities.noString(s))
742          s = s + " & ";
743        s = s + f.getProperty()+" "+(f.hasOp() ? f.getOp().toCode() : "?")+" "+f.getValue();
744      }
745      return "from "+system+" where "+s+vsd;
746    }
747    return "All codes from "+system+vsd;
748  }
749
750  public String summary(Coding code) {
751    return code.getSystem()+"#"+code.getCode()+": \""+code.getDisplay()+"\"";
752  }
753
754  public String summary(CodeableConcept code) {
755    StringBuilder b = new StringBuilder();
756    b.append("{");
757    boolean first = true;
758    for (Coding c : code.getCoding()) {
759      if (first) first = false; else b.append(",");
760      b.append(summary(c));
761    }
762    b.append("}: \"");
763    b.append(code.getText());
764    b.append("\"");
765    return b.toString();
766  }
767
768  public void removeCS(String url) {
769    synchronized (lock) {
770      String name = getSystemNameKeyGenerator().getNameForSystem(url);
771      if (caches.containsKey(name)) {
772        caches.remove(name);
773      }
774    }   
775  }
776
777  public String getFolder() {
778    return folder;
779  }
780
781
782}