001package org.hl7.fhir.r4.context;
002
003import java.io.File;
004import java.io.FileNotFoundException;
005import java.io.FileOutputStream;
006import java.io.FileWriter;
007import java.io.IOException;
008import java.io.OutputStreamWriter;
009import java.nio.charset.Charset;
010import java.text.DateFormat;
011import java.text.ParseException;
012import java.text.SimpleDateFormat;
013import java.util.ArrayList;
014import java.util.Date;
015import java.util.HashMap;
016import java.util.List;
017import java.util.Map;
018
019import org.apache.commons.codec.Charsets;
020import org.apache.commons.lang3.StringUtils;
021import org.hl7.fhir.exceptions.FHIRException;
022import org.hl7.fhir.exceptions.FHIRFormatError;
023import org.hl7.fhir.r4.context.IWorkerContext.ValidationResult;
024import org.hl7.fhir.r4.formats.IParser.OutputStyle;
025import org.hl7.fhir.r4.formats.JsonParser;
026import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent;
027import org.hl7.fhir.r4.model.CodeableConcept;
028import org.hl7.fhir.r4.model.Coding;
029import org.hl7.fhir.r4.model.UriType;
030import org.hl7.fhir.r4.model.ValueSet;
031import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent;
032import org.hl7.fhir.r4.model.ValueSet.ConceptSetFilterComponent;
033import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent;
034import org.hl7.fhir.r4.terminologies.ValueSetExpander.TerminologyServiceErrorClass;
035import org.hl7.fhir.r4.terminologies.ValueSetExpander.ValueSetExpansionOutcome;
036import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
037import org.hl7.fhir.utilities.TextFile;
038import org.hl7.fhir.utilities.Utilities;
039import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
040
041import com.google.gson.JsonElement;
042import com.google.gson.JsonNull;
043import com.google.gson.JsonObject;
044import com.google.gson.JsonPrimitive;
045
046/**
047 * This implements a two level cache. 
048 *  - a temporary cache for remmbering previous local operations
049 *  - a persistent cache for rembering tx server operations
050 *  
051 * the cache is a series of pairs: a map, and a list. the map is the loaded cache, the list is the persiistent cache, carefully maintained in order for version control consistency
052 * 
053 * @author graha
054 *
055 */
056public class TerminologyCache {
057  public static final boolean TRANSIENT = false;
058  public static final boolean PERMANENT = true;
059  private static final String NAME_FOR_NO_SYSTEM = "all-systems";
060  private static final String ENTRY_MARKER = "-------------------------------------------------------------------------------------";
061  private static final String BREAK = "####";
062
063  public class CacheToken {
064    private String name;
065    private String key;
066    private String request;
067    public void setName(String n) {
068      if (name == null)
069        name = n;
070      else if (!n.equals(name))
071        name = NAME_FOR_NO_SYSTEM;
072    }
073  }
074
075  private class CacheEntry {
076    private String request;
077    private boolean persistent;
078    private ValidationResult v;
079    private ValueSetExpansionOutcome e;
080  }
081  
082  private class NamedCache {
083    private String name; 
084    private List<CacheEntry> list = new ArrayList<CacheEntry>(); // persistent entries
085    private Map<String, CacheEntry> map = new HashMap<String, CacheEntry>();
086  }
087  
088
089  private Object lock;
090  private String folder;
091  private Map<String, NamedCache> caches = new HashMap<String, NamedCache>();
092  
093  // use lock from the context
094  public TerminologyCache(Object lock, String folder) throws FileNotFoundException, IOException, FHIRException {
095    super();
096    this.lock = lock;
097    this.folder = folder;
098    if (folder != null)
099      load();
100  }
101  
102  public CacheToken generateValidationToken(Coding code, ValueSet vs) {
103    CacheToken ct = new CacheToken();
104    if (code.hasSystem())
105      ct.name = getNameForSystem(code.getSystem());
106    else
107      ct.name = NAME_FOR_NO_SYSTEM;
108    JsonParser json = new JsonParser();
109    json.setOutputStyle(OutputStyle.PRETTY);
110    ValueSet vsc = getVSEssense(vs);
111    try {
112      ct.request = "{\"code\" : "+json.composeString(code, "code")+", \"valueSet\" :"+(vsc == null ? "null" : json.composeString(vsc))+"}";
113    } catch (IOException e) {
114      throw new Error(e);
115    }
116    ct.key = String.valueOf(hashNWS(ct.request));
117    return ct;
118  }
119
120  public CacheToken generateValidationToken(CodeableConcept code, ValueSet vs) {
121    CacheToken ct = new CacheToken();
122    for (Coding c : code.getCoding()) {
123      if (c.hasSystem())
124        ct.setName(getNameForSystem(c.getSystem()));
125    }
126    JsonParser json = new JsonParser();
127    json.setOutputStyle(OutputStyle.PRETTY);
128    ValueSet vsc = getVSEssense(vs);
129    try {
130      ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+", \"valueSet\" :"+json.composeString(vsc)+"}";
131    } catch (IOException e) {
132      throw new Error(e);
133    }
134    ct.key = String.valueOf(hashNWS(ct.request));
135    return ct;
136  }
137  
138  public ValueSet getVSEssense(ValueSet vs) {
139    if (vs == null)
140      return null;
141    ValueSet vsc = new ValueSet();
142    vsc.setCompose(vs.getCompose());
143    if (vs.hasExpansion()) {
144      vsc.getExpansion().getParameter().addAll(vs.getExpansion().getParameter());
145      vsc.getExpansion().getContains().addAll(vs.getExpansion().getContains());
146    }
147    return vsc;
148  }
149
150  public CacheToken generateExpandToken(ValueSet vs, boolean heirarchical) {
151    CacheToken ct = new CacheToken();
152    ValueSet vsc = getVSEssense(vs);
153    for (ConceptSetComponent inc : vs.getCompose().getInclude())
154      if (inc.hasSystem())
155        ct.setName(getNameForSystem(inc.getSystem()));
156    for (ConceptSetComponent inc : vs.getCompose().getExclude())
157      if (inc.hasSystem())
158        ct.setName(getNameForSystem(inc.getSystem()));
159    for (ValueSetExpansionContainsComponent inc : vs.getExpansion().getContains())
160      if (inc.hasSystem())
161        ct.setName(getNameForSystem(inc.getSystem()));
162    JsonParser json = new JsonParser();
163    json.setOutputStyle(OutputStyle.PRETTY);
164    try {
165      ct.request = "{\"hierarchical\" : "+(heirarchical ? "true" : "false")+", \"valueSet\" :"+json.composeString(vsc)+"}\r\n";
166    } catch (IOException e) {
167      throw new Error(e);
168    }
169    ct.key = String.valueOf(hashNWS(ct.request));
170    return ct;
171  }
172
173  private String getNameForSystem(String system) {
174    if (system.equals("http://snomed.info/sct"))
175      return "snomed";
176    if (system.equals("http://www.nlm.nih.gov/research/umls/rxnorm"))
177      return "rxnorm";
178    if (system.equals("http://loinc.org"))
179      return "loinc";
180    if (system.equals("http://unitsofmeasure.org"))
181      return "ucum";
182    if (system.startsWith("http://hl7.org/fhir/sid/"))
183      return system.substring(24).replace("/", "");
184    if (system.startsWith("urn:iso:std:iso:"))
185      return "iso"+system.substring(16).replace(":", "");
186    if (system.startsWith("http://terminology.hl7.org/CodeSystem/"))
187      return system.substring(38).replace("/", "");
188    if (system.startsWith("http://hl7.org/fhir/"))
189      return system.substring(20).replace("/", "");
190    if (system.equals("urn:ietf:bcp:47"))
191      return "lang";
192    if (system.equals("urn:ietf:bcp:13"))
193      return "mimetypes";
194    if (system.equals("urn:iso:std:iso:11073:10101"))
195      return "11073";
196    if (system.equals("http://dicom.nema.org/resources/ontology/DCM"))
197      return "dicom";
198    return system.replace("/", "_").replace(":", "_");
199  }
200
201  public NamedCache getNamedCache(CacheToken cacheToken) {
202    NamedCache nc = caches.get(cacheToken.name);
203    if (nc == null) {
204      nc = new NamedCache();
205      nc.name = cacheToken.name;
206      caches.put(nc.name, nc);
207    }
208    return nc;
209  }
210  
211  public ValueSetExpansionOutcome getExpansion(CacheToken cacheToken) {
212    synchronized (lock) {
213      NamedCache nc = getNamedCache(cacheToken);
214      CacheEntry e = nc.map.get(cacheToken.key);
215      if (e == null)
216        return null;
217      else
218        return e.e;
219    }
220  }
221
222  public void cacheExpansion(CacheToken cacheToken, ValueSetExpansionOutcome res, boolean persistent) {
223    synchronized (lock) {      
224      NamedCache nc = getNamedCache(cacheToken);
225      CacheEntry e = new CacheEntry();
226      e.request = cacheToken.request;
227      e.persistent = persistent;
228      e.e = res;
229      store(cacheToken, persistent, nc, e);
230    }    
231  }
232
233  public void store(CacheToken cacheToken, boolean persistent, NamedCache nc, CacheEntry e) {
234    boolean n = nc.map.containsKey(cacheToken.key);
235    nc.map.put(cacheToken.key, e);
236    if (persistent) {
237      if (n) {
238        for (int i = nc.list.size()- 1; i>= 0; i--) {
239          if (nc.list.get(i).request.equals(e.request)) {
240            nc.list.remove(i);
241          }
242        }
243      }
244      nc.list.add(e);
245      save(nc);  
246    }
247  }
248
249  public ValidationResult getValidation(CacheToken cacheToken) {
250    synchronized (lock) {
251      NamedCache nc = getNamedCache(cacheToken);
252      CacheEntry e = nc.map.get(cacheToken.key);
253      if (e == null)
254        return null;
255      else
256        return e.v;
257    }
258  }
259
260  public void cacheValidation(CacheToken cacheToken, ValidationResult res, boolean persistent) {
261    synchronized (lock) {      
262      NamedCache nc = getNamedCache(cacheToken);
263      CacheEntry e = new CacheEntry();
264      e.request = cacheToken.request;
265      e.persistent = persistent;
266      e.v = res;
267      store(cacheToken, persistent, nc, e);
268    }    
269  }
270
271  
272  // persistence
273  
274  public void save() {
275    
276  }
277  
278  private void save(NamedCache nc) {
279    if (folder == null)
280      return;
281    
282    try {
283      OutputStreamWriter sw = new OutputStreamWriter(new FileOutputStream(Utilities.path(folder, nc.name+".cache")), "UTF-8");
284      sw.write(ENTRY_MARKER+"\r\n");
285      JsonParser json = new JsonParser();
286      json.setOutputStyle(OutputStyle.PRETTY);
287      for (CacheEntry ce : nc.list) {
288        sw.write(ce.request.trim());
289        sw.write(BREAK+"\r\n");
290        if (ce.e != null) {
291          sw.write("e: {\r\n");
292          if (ce.e.getValueset() != null)
293            sw.write("  \"valueSet\" : "+json.composeString(ce.e.getValueset()).trim()+",\r\n");
294          sw.write("  \"error\" : \""+Utilities.escapeJson(ce.e.getError()).trim()+"\"\r\n}\r\n");
295        } else {
296          sw.write("v: {\r\n");
297          sw.write("  \"display\" : \""+Utilities.escapeJson(ce.v.getDisplay()).trim()+"\",\r\n");
298          sw.write("  \"severity\" : "+(ce.v.getSeverity() == null ? "null" : "\""+ce.v.getSeverity().toCode().trim()+"\"")+",\r\n");
299          sw.write("  \"error\" : \""+Utilities.escapeJson(ce.v.getMessage()).trim()+"\"\r\n}\r\n");
300        }
301        sw.write(ENTRY_MARKER+"\r\n");
302      }      
303      sw.close();
304    } catch (Exception e) {
305      System.out.println("error saving "+nc.name+": "+e.getMessage());
306    }
307  }
308
309  private void load() throws FHIRException {
310    for (String fn : new File(folder).list()) {
311      if (fn.endsWith(".cache") && !fn.equals("validation.cache")) {
312        try {
313          //  System.out.println("Load "+fn);
314          String title = fn.substring(0, fn.lastIndexOf("."));
315          NamedCache nc = new NamedCache();
316          nc.name = title;
317          caches.put(title, nc);
318          String src = TextFile.fileToString(Utilities.path(folder, fn));
319          if (src.startsWith("?"))
320            src = src.substring(1);
321          int i = src.indexOf(ENTRY_MARKER); 
322          while (i > -1) {
323            String s = src.substring(0, i);
324            src = src.substring(i+ENTRY_MARKER.length()+1);
325            i = src.indexOf(ENTRY_MARKER);
326            if (!Utilities.noString(s)) {
327              int j = s.indexOf(BREAK);
328              String q = s.substring(0, j);
329              String p = s.substring(j+BREAK.length()+1).trim();
330              CacheEntry ce = new CacheEntry();
331              ce.persistent = true;
332              ce.request = q;
333              boolean e = p.charAt(0) == 'e';
334              p = p.substring(3);
335              JsonObject o = (JsonObject) new com.google.gson.JsonParser().parse(p);
336              String error = loadJS(o.get("error"));
337              if (e) {
338                if (o.has("valueSet"))
339                  ce.e = new ValueSetExpansionOutcome((ValueSet) new JsonParser().parse(o.getAsJsonObject("valueSet")), error, TerminologyServiceErrorClass.UNKNOWN);
340                else
341                  ce.e = new ValueSetExpansionOutcome(error, TerminologyServiceErrorClass.UNKNOWN);
342              } else {
343                IssueSeverity severity = o.get("severity") instanceof JsonNull ? null :  IssueSeverity.fromCode(o.get("severity").getAsString());
344                String display = loadJS(o.get("display"));
345                ce.v = new ValidationResult(severity, error, new ConceptDefinitionComponent().setDisplay(display));
346              }
347              nc.map.put(String.valueOf(hashNWS(ce.request)), ce);
348              nc.list.add(ce);
349            }
350          }        
351        } catch (Exception e) {
352          throw new FHIRException("Error loading "+fn+": "+e.getMessage(), e);
353        }
354      }
355    }
356  }
357  
358  private String loadJS(JsonElement e) {
359    if (e == null)
360      return null;
361    if (!(e instanceof JsonPrimitive))
362      return null;
363    String s = e.getAsString();
364    if ("".equals(s))
365      return null;
366    return s;
367  }
368
369  private String hashNWS(String s) {
370    return String.valueOf(s.replace("\r", "").replace("\n", "").replace(" ", "").hashCode());
371  }
372
373  // management
374  
375  public TerminologyCache copy() {
376    // TODO Auto-generated method stub
377    return null;
378  }
379  
380  public String summary(ValueSet vs) {
381    if (vs == null)
382      return "null";
383    
384    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
385    for (ConceptSetComponent cc : vs.getCompose().getInclude())
386      b.append("Include "+getIncSummary(cc));
387    for (ConceptSetComponent cc : vs.getCompose().getExclude())
388      b.append("Exclude "+getIncSummary(cc));
389    return b.toString();
390  }
391
392  private String getIncSummary(ConceptSetComponent cc) {
393    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
394    for (UriType vs : cc.getValueSet())
395      b.append(vs.asStringValue());
396    String vsd = b.length() > 0 ? " where the codes are in the value sets ("+b.toString()+")" : "";
397    String system = cc.getSystem();
398    if (cc.hasConcept())
399      return Integer.toString(cc.getConcept().size())+" codes from "+system+vsd;
400    if (cc.hasFilter()) {
401      String s = "";
402      for (ConceptSetFilterComponent f : cc.getFilter()) {
403        if (!Utilities.noString(s))
404          s = s + " & ";
405        s = s + f.getProperty()+" "+f.getOp().toCode()+" "+f.getValue();
406      }
407      return "from "+system+" where "+s+vsd;
408    }
409    return "All codes from "+system+vsd;
410  }
411
412  public String summary(Coding code) {
413    return code.getSystem()+"#"+code.getCode()+": \""+code.getDisplay()+"\"";
414  }
415
416
417  public String summary(CodeableConcept code) {
418    StringBuilder b = new StringBuilder();
419    b.append("{");
420    boolean first = true;
421    for (Coding c : code.getCoding()) {
422      if (first) first = false; else b.append(",");
423      b.append(summary(c));
424    }
425    b.append("}: \"");
426    b.append(code.getText());
427    b.append("\"");
428    return b.toString();
429  }
430  
431}