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