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