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