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