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}