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}