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