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