
001package org.hl7.fhir.r5.context; 002 003import java.util.*; 004 005import org.hl7.fhir.exceptions.FHIRException; 006import org.hl7.fhir.r5.model.CanonicalResource; 007import org.hl7.fhir.r5.model.CodeSystem; 008import org.hl7.fhir.r5.model.Enumerations.CodeSystemContentMode; 009import org.hl7.fhir.r5.model.PackageInformation; 010import org.hl7.fhir.r5.model.StructureDefinition; 011import org.hl7.fhir.r5.terminologies.CodeSystemUtilities; 012import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage; 013import org.hl7.fhir.utilities.VersionUtilities; 014 015/** 016 * This manages a cached list of resources, and provides high speed access by URL / URL+version, and assumes that patch version doesn't matter for access 017 * note, though, that not all resources have semver versions 018 * 019 * @author graha 020 * 021 */ 022 023@MarkedToMoveToAdjunctPackage 024public class CanonicalResourceManager<T extends CanonicalResource> { 025 026 private final String[] INVALID_TERMINOLOGY_URLS = { 027 "http://snomed.info/sct", 028 "http://dicom.nema.org/resources/ontology/DCM", 029 "http://nucc.org/provider-taxonomy" 030 }; 031 032 public static abstract class CanonicalResourceProxy { 033 private String type; 034 private String id; 035 private String url; 036 private String version; 037 private String supplements; 038 private String derivation; 039 private CanonicalResource resource; 040 private boolean hacked; 041 private String content; 042 043 public CanonicalResourceProxy(String type, String id, String url, String version, String supplements, String derivation, String content) { 044 super(); 045 this.type = type; 046 this.id = id; 047 this.url = url; 048 this.version = version; 049 this.supplements = supplements; 050 this.content = content; 051 this.derivation = derivation; 052 } 053 054 public String getType() { 055 return type; 056 } 057 058 public String getId() { 059 return id; 060 } 061 062 public String getUrl() { 063 return url; 064 } 065 066 public String getVersion() { 067 return version; 068 } 069 070 public boolean hasId() { 071 return id != null; 072 } 073 074 public boolean hasUrl() { 075 return url != null; 076 } 077 078 public boolean hasVersion() { 079 return version != null; 080 } 081 082 public String getSupplements() { 083 return supplements; 084 } 085 086 087 public String getContent() { 088 return content; 089 } 090 091 public String getDerivation() { 092 return derivation; 093 } 094 095 public void setDerivation(String derivation) { 096 this.derivation = derivation; 097 } 098 099 public CanonicalResource getResource() throws FHIRException { 100 if (resource == null) { 101 resource = loadResource(); 102 if (hacked) { 103 resource.setUrl(url).setVersion(version); 104 } 105 if (resource instanceof CodeSystem) { 106 CodeSystemUtilities.crossLinkCodeSystem((CodeSystem) resource); 107 } 108 } 109 return resource; 110 } 111 112 public void setResource(CanonicalResource resource) { 113 this.resource = resource; 114 } 115 116 public abstract CanonicalResource loadResource() throws FHIRException; 117 118 @Override 119 public String toString() { 120 return type+"/"+id+": "+url+"|"+version; 121 } 122 123 public void hack(String url, String version) { 124 this.url = url; 125 this.version = version; 126 this.hacked = true; 127 128 } 129 } 130 131 public static class CanonicalListSorter implements Comparator<CanonicalResource> { 132 133 @Override 134 public int compare(CanonicalResource arg0, CanonicalResource arg1) { 135 String u0 = arg0.getUrl(); 136 String u1 = arg1.getUrl(); 137 return u0.compareTo(u1); 138 } 139 } 140 141 public class CachedCanonicalResource<T1 extends CanonicalResource> { 142 private T1 resource; 143 private CanonicalResourceProxy proxy; 144 private PackageInformation packageInfo; 145 146 public CachedCanonicalResource(T1 resource, PackageInformation packageInfo) { 147 super(); 148 this.resource = resource; 149 this.packageInfo = packageInfo; 150 } 151 152 public CachedCanonicalResource(CanonicalResourceProxy proxy, PackageInformation packageInfo) { 153 super(); 154 this.proxy = proxy; 155 this.packageInfo = packageInfo; 156 } 157 158 public T1 getResource() { 159 if (resource == null) { 160 @SuppressWarnings("unchecked") 161 T1 res = (T1) proxy.getResource(); 162 if (res == null) { 163 throw new Error("Proxy loading a resource from "+packageInfo+" failed and returned null"); 164 } 165 synchronized (this) { 166 resource = res; 167 } 168 resource.setSourcePackage(packageInfo); 169 proxy = null; 170 } 171 return resource; 172 } 173 174 public PackageInformation getPackageInfo() { 175 return packageInfo; 176 } 177 public String getUrl() { 178 return resource != null ? resource.getUrl() : proxy.getUrl(); 179 } 180 public String getId() { 181 return resource != null ? resource.getId() : proxy.getId(); 182 } 183 public String getVersion() { 184 return resource != null ? resource.getVersion() : proxy.getVersion(); 185 } 186 public boolean hasVersion() { 187 return resource != null ? resource.hasVersion() : proxy.getVersion() != null; 188 } 189 public String getContent() { 190 if (resource != null && resource instanceof CodeSystem) { 191 CodeSystemContentMode cnt = ((CodeSystem) resource).getContent(); 192 return cnt == null ? null : cnt.toCode(); 193 } else if (proxy != null) { 194 return proxy.getContent(); 195 } else { 196 return null; 197 } 198 } 199 200 @Override 201 public String toString() { 202 return resource != null ? resource.fhirType()+"/"+resource.getId()+"["+resource.getUrl()+"|"+resource.getVersion()+"]" : proxy.toString(); 203 } 204 205 public String supplements() { 206 if (resource == null) { 207 return proxy.getSupplements(); 208 } else { 209 return resource instanceof CodeSystem ? ((CodeSystem) resource).getSupplements() : null; 210 } 211 } 212 213 public Object getDerivation() { 214 if (resource == null) { 215 return proxy.getDerivation(); 216 } else { 217 return resource instanceof StructureDefinition ? ((StructureDefinition) resource).getDerivationElement().primitiveValue() : null; 218 } 219 } 220 221 public void unload() { 222 if (proxy != null) { 223 resource = null; 224 } 225 } 226 } 227 228 public class MetadataResourceVersionComparator<T1 extends CachedCanonicalResource<T>> implements Comparator<T1> { 229 @Override 230 public int compare(T1 arg1, T1 arg2) { 231 String c1 = arg1.getContent(); 232 String c2 = arg2.getContent(); 233 if (c1 != null && c2 != null && !c1.equals(c2)) { 234 int i1 = orderOfContent(c1); 235 int i2 = orderOfContent(c2); 236 return Integer.compare(i1, i2); 237 } 238 String v1 = arg1.getVersion(); 239 String v2 = arg2.getVersion(); 240 if (v1 == null && v2 == null) { 241 return Integer.compare(list.indexOf(arg1), list.indexOf(arg2)); // retain original order 242 } else if (v1 == null) { 243 return -1; 244 } else if (v2 == null) { 245 return 1; 246 } else { 247 String mm1 = VersionUtilities.getMajMin(v1); 248 String mm2 = VersionUtilities.getMajMin(v2); 249 if (mm1 == null || mm2 == null) { 250 return v1.compareTo(v2); 251 } else { 252 return mm1.compareTo(mm2); 253 } 254 } 255 } 256 257 private int orderOfContent(String c) { 258 switch (c) { 259 case "not-present": return 1; 260 case "example": return 2; 261 case "fragment": return 3; 262 case "complete": return 5; 263 case "supplement": return 4; 264 } 265 return 0; 266 } 267 } 268 269 private boolean minimalMemory; 270 private boolean enforceUniqueId; 271 private List<CachedCanonicalResource<T>> list = new ArrayList<>(); 272 private Map<String, List<CachedCanonicalResource<T>>> listForId; 273 private Map<String, List<CachedCanonicalResource<T>>> listForUrl; 274 private Map<String, CachedCanonicalResource<T>> map; 275 private Map<String, List<CachedCanonicalResource<T>>> supplements; // general index based on CodeSystem.supplements 276 private String version; // for debugging purposes 277 278 279 public CanonicalResourceManager(boolean enforceUniqueId, boolean minimalMemory) { 280 super(); 281 this.enforceUniqueId = enforceUniqueId; 282 this.minimalMemory = minimalMemory; 283 list = new ArrayList<>(); 284 listForId = new HashMap<>(); 285 listForUrl = new HashMap<>(); 286 map = new HashMap<>(); 287 supplements = new HashMap<>(); // general index based on CodeSystem.supplements 288 } 289 290 291 public String getVersion() { 292 return version; 293 } 294 295 296 public void setVersion(String version) { 297 this.version = version; 298 } 299 300 301 public void copy(CanonicalResourceManager<T> source) { 302 list.clear(); 303 map.clear(); 304 list.addAll(source.list); 305 map.putAll(source.map); 306 } 307 308 public void register(CanonicalResourceProxy r, PackageInformation packgeInfo) { 309 if (!r.hasId()) { 310 throw new FHIRException("An id is required for a deferred load resource"); 311 } 312 CanonicalResourceManager<T>.CachedCanonicalResource<T> cr = new CachedCanonicalResource<T>(r, packgeInfo); 313 see(cr); 314 } 315 316 public void see(T r, PackageInformation packgeInfo) { 317 if (r != null) { 318 if (!r.hasId()) { 319 r.setId(UUID.randomUUID().toString()); 320 } 321 CanonicalResourceManager<T>.CachedCanonicalResource<T> cr = new CachedCanonicalResource<T>(r, packgeInfo); 322 see(cr); 323 } 324 } 325 326 public void see(CachedCanonicalResource<T> cr) { 327 // -- 1. exit conditions ----------------------------------------------------------------------------- 328 329 // ignore UTG NUCC erroneous code system 330 if (cr.getPackageInfo() != null 331 && cr.getPackageInfo().getId() != null 332 && cr.getPackageInfo().getId().startsWith("hl7.terminology") 333 && Arrays.stream(INVALID_TERMINOLOGY_URLS).anyMatch((it)->it.equals(cr.getUrl()))) { 334 return; 335 } 336 if (map.get(cr.getUrl()) != null && (cr.getPackageInfo() != null && cr.getPackageInfo().isExamplesPackage())) { 337 return; 338 } 339 340 // -- 2. preparation ----------------------------------------------------------------------------- 341 if (cr.resource != null && cr.getPackageInfo() != null) { 342 cr.resource.setSourcePackage(cr.getPackageInfo()); 343 } 344 345 // -- 3. deleting existing content --------------------------------------------------------------- 346 if (enforceUniqueId && map.containsKey(cr.getId())) { 347 drop(cr.getId()); 348 } 349 350 // special case logic for UTG support prior to version 5 351 if (cr.getPackageInfo() != null && cr.getPackageInfo().getId().startsWith("hl7.terminology")) { 352 List<CachedCanonicalResource<T>> toDrop = new ArrayList<>(); 353 for (CachedCanonicalResource<T> n : list) { 354 if (n.getUrl() != null && n.getUrl().equals(cr.getUrl()) && isBasePackage(n.getPackageInfo())) { 355 toDrop.add(n); 356 } 357 } 358 for (CachedCanonicalResource<T> n : toDrop) { 359 drop(n); 360 } 361 } 362// CachedCanonicalResource<T> existing = cr.hasVersion() ? map.get(cr.getUrl()+"|"+cr.getVersion()) : map.get(cr.getUrl()+"|#0"); 363// if (existing != null) { 364// drop(existing); // was list.remove(existing) 365// } 366 367 // -- 4. ok we add it to the list --------------------------------------------------------------- 368 if (!enforceUniqueId) { 369 if (!listForId.containsKey(cr.getId())) { 370 listForId.put(cr.getId(), new ArrayList<>()); 371 } 372 List<CachedCanonicalResource<T>> set = listForId.get(cr.getId()); 373 set.add(cr); 374 } 375 list.add(cr); 376 if (!listForUrl.containsKey(cr.getUrl())) { 377 listForUrl.put(cr.getUrl(), new ArrayList<>()); 378 } 379 addToSupplements(cr); 380 List<CachedCanonicalResource<T>> set = listForUrl.get(cr.getUrl()); 381 set.add(cr); 382 if (set.size() > 1) { 383 Collections.sort(set, new MetadataResourceVersionComparator<CachedCanonicalResource<T>>()); 384 } 385 386 // -- 4. add to the map all the ways --------------------------------------------------------------- 387 String pv = cr.getPackageInfo() != null ? cr.getPackageInfo().getVID() : null; 388 map.put(cr.getId(), cr); // we do this so we can drop by id - if not enforcing id, it's just the most recent resource with this id 389 map.put(cr.hasVersion() ? cr.getUrl()+"|"+cr.getVersion() : cr.getUrl()+"|#0", cr); 390 if (pv != null) { 391 map.put(pv+":"+(cr.hasVersion() ? cr.getUrl()+"|"+cr.getVersion() : cr.getUrl()+"|#0"), cr); 392 } 393 int ndx = set.indexOf(cr); 394 if (ndx == set.size()-1) { 395 map.put(cr.getUrl(), cr); 396 if (pv != null) { 397 map.put(pv+":"+cr.getUrl(), cr); 398 } 399 } 400 String mm = VersionUtilities.getMajMin(cr.getVersion()); 401 if (mm != null) { 402 if (pv != null) { 403 map.put(pv+":"+cr.getUrl()+"|"+mm, cr); 404 } 405 if (set.size() - 1 == ndx) { 406 map.put(cr.getUrl()+"|"+mm, cr); 407 } else { 408 for (int i = set.size() - 1; i > ndx; i--) { 409 if (mm.equals(VersionUtilities.getMajMin(set.get(i).getVersion()))) { 410 return; 411 } 412 map.put(cr.getUrl()+"|"+mm, cr); 413 } 414 } 415 } 416 } 417 418 private void addToSupplements(CanonicalResourceManager<T>.CachedCanonicalResource<T> cr) { 419 String surl = cr.supplements(); 420 if (surl != null) { 421 List<CanonicalResourceManager<T>.CachedCanonicalResource<T>> list = supplements.get(surl); 422 if (list == null) { 423 list = new ArrayList<>(); 424 supplements.put(surl, list); 425 } 426 list.add(cr); 427 } 428 } 429 430 431 public void drop(CachedCanonicalResource<T> cr) { 432 while (map.values().remove(cr)); 433 while (listForId.values().remove(cr)); 434 while (listForUrl.values().remove(cr)); 435 String surl = cr.supplements(); 436 if (surl != null) { 437 supplements.get(surl).remove(cr); 438 } 439 list.remove(cr); 440 List<CachedCanonicalResource<T>> set = listForUrl.get(cr.getUrl()); 441 if (set != null) { // it really should be 442 boolean last = set.indexOf(cr) == set.size()-1; 443 set.remove(cr); 444 if (!set.isEmpty()) { 445 CachedCanonicalResource<T> crl = set.get(set.size()-1); 446 if (last) { 447 map.put(crl.getUrl(), crl); 448 } 449 String mm = VersionUtilities.getMajMin(cr.getVersion()); 450 if (mm != null) { 451 for (int i = set.size()-1; i >= 0; i--) { 452 if (mm.equals(VersionUtilities.getMajMin(set.get(i).getVersion()))) { 453 map.put(cr.getUrl()+"|"+mm, set.get(i)); 454 break; 455 } 456 } 457 } 458 } 459 } 460 } 461 462 public void drop(String id) { 463 if (enforceUniqueId) { 464 CachedCanonicalResource<T> cr = map.get(id); 465 if (cr != null) { 466 drop(cr); 467 } 468 } else { 469 List<CachedCanonicalResource<T>> set = listForId.get(id); 470 if (set != null) { // it really should be 471 for (CachedCanonicalResource<T> i : set) { 472 drop(i); 473 } 474 } 475 } 476 } 477 478 private boolean isBasePackage(PackageInformation packageInfo) { 479 return packageInfo == null ? false : VersionUtilities.isCorePackage(packageInfo.getId()); 480 } 481 482 private void updateList(String url, String version) { 483 List<CachedCanonicalResource<T>> rl = new ArrayList<>(); 484 for (CachedCanonicalResource<T> t : list) { 485 if (url.equals(t.getUrl()) && !rl.contains(t)) { 486 rl.add(t); 487 } 488 } 489 if (rl.size() > 0) { 490 // sort by version as much as we are able 491 // the current is the latest 492 map.put(url, rl.get(rl.size()-1)); 493 // now, also, the latest for major/minor 494 if (version != null) { 495 CachedCanonicalResource<T> latest = null; 496 for (CachedCanonicalResource<T> t : rl) { 497 if (VersionUtilities.versionsCompatible(t.getVersion(), version)) { 498 latest = t; 499 } 500 } 501 if (latest != null) { // might be null if it's not using semver 502 String lv = VersionUtilities.getMajMin(latest.getVersion()); 503 if (lv != null && !lv.equals(version)) 504 map.put(url+"|"+lv, rl.get(rl.size()-1)); 505 } 506 } 507 } 508 } 509 510 511 public boolean has(String url) { 512 return map.containsKey(url); 513 } 514 515 public boolean has(String system, String version) { 516 if (map.containsKey(system+"|"+version)) 517 return true; 518 String mm = VersionUtilities.getMajMin(version); 519 if (mm != null) 520 return map.containsKey(system+"|"+mm); 521 else 522 return false; 523 } 524 525 public T get(String url) { 526 return map.containsKey(url) ? map.get(url).getResource() : null; 527 } 528 529 public T get(String system, String version) { 530 if (version == null) { 531 return get(system); 532 } else { 533 if (map.containsKey(system+"|"+version)) 534 return map.get(system+"|"+version).getResource(); 535 String mm = VersionUtilities.getMajMin(version); 536 if (mm != null && map.containsKey(system+"|"+mm)) 537 return map.get(system+"|"+mm).getResource(); 538 else 539 return null; 540 } 541 } 542 543 public List<T> getForUrl(String url) { 544 List<T> res = new ArrayList<>(); 545 List<CanonicalResourceManager<T>.CachedCanonicalResource<T>> list = listForUrl.get(url); 546 if (list != null) { 547 for (CanonicalResourceManager<T>.CachedCanonicalResource<T> t : list) { 548 res.add(t.getResource()); 549 } 550 } 551 return res; 552 } 553 554 /** 555 * This is asking for a packaged version aware resolution 556 * 557 * if we can resolve the reference in the package dependencies, we will. if we can't 558 * then we fall back to the non-package approach 559 * 560 * The context has to prepare the pvlist based on the original package 561 * @param url 562 * @param srcInfo 563 * @return 564 */ 565 public T get(String url, List<String> pvlist) { 566 for (String pv : pvlist) { 567 if (map.containsKey(pv+":"+url)) { 568 return map.get(pv+":"+url).getResource(); 569 } 570 } 571 return map.containsKey(url) ? map.get(url).getResource() : null; 572 } 573 574 public T get(String system, String version, List<String> pvlist) { 575 if (version == null) { 576 return get(system, pvlist); 577 } else { 578 for (String pv : pvlist) { 579 if (map.containsKey(pv+":"+system+"|"+version)) 580 return map.get(pv+":"+system+"|"+version).getResource(); 581 } 582 String mm = VersionUtilities.getMajMin(version); 583 if (mm != null && map.containsKey(system+"|"+mm)) 584 for (String pv : pvlist) { 585 if (map.containsKey(pv+":"+system+"|"+mm)) 586 return map.get(pv+":"+system+"|"+mm).getResource(); 587 } 588 589 if (map.containsKey(system+"|"+version)) 590 return map.get(system+"|"+version).getResource(); 591 if (mm != null && map.containsKey(system+"|"+mm)) 592 return map.get(system+"|"+mm).getResource(); 593 else 594 return null; 595 } 596 } 597 598 599 600 public PackageInformation getPackageInfo(String system, String version) { 601 if (version == null) { 602 return map.containsKey(system) ? map.get(system).getPackageInfo() : null; 603 } else { 604 if (map.containsKey(system+"|"+version)) 605 return map.get(system+"|"+version).getPackageInfo(); 606 String mm = VersionUtilities.getMajMin(version); 607 if (mm != null && map.containsKey(system+"|"+mm)) 608 return map.get(system+"|"+mm).getPackageInfo(); 609 else 610 return null; 611 } 612 } 613 614 615 616 617 public int size() { 618 return list.size(); 619 } 620 621 622 623 public void listAll(List<T> result) { 624 for (CachedCanonicalResource<T> t : list) { 625 result.add(t.getResource()); 626 } 627 } 628 629 public void listAllM(List<CanonicalResource> result) { 630 for (CachedCanonicalResource<T> t : list) { 631 result.add(t.getResource()); 632 } 633 } 634 635 public List<T> getSupplements(T cr) { 636 if (cr == null) { 637 return new ArrayList<T>(); 638 } 639 if (cr.hasSourcePackage()) { 640 List<String> pvl = new ArrayList<>(); 641 pvl.add(cr.getSourcePackage().getVID()); 642 return getSupplements(cr.getUrl(), cr.getVersion(), pvl); 643 } else { 644 return getSupplements(cr.getUrl(), cr.getVersion(), null); 645 } 646 } 647 648 public List<T> getSupplements(String url) { 649 return getSupplements(url, null, null); 650 } 651 652 public List<T> getSupplements(String url, String version) { 653 return getSupplements(url, version, null); 654 } 655 656 public List<T> getSupplements(String url, String version, List<String> pvlist) { 657 boolean possibleMatches = false; 658 List<T> res = new ArrayList<>(); 659 if (version != null) { 660 List<CanonicalResourceManager<T>.CachedCanonicalResource<T>> list = supplements.get(url+"|"+version); 661 if (list != null) { 662 for (CanonicalResourceManager<T>.CachedCanonicalResource<T> t : list) { 663 possibleMatches = true; 664 if (pvlist == null || pvlist.contains(t.getPackageInfo().getVID())) { 665 res.add(t.getResource()); 666 } 667 } 668 } 669 } 670 List<CanonicalResourceManager<T>.CachedCanonicalResource<T>> list = supplements.get(url); 671 if (list != null) { 672 for (CanonicalResourceManager<T>.CachedCanonicalResource<T> t : list) { 673 possibleMatches = true; 674 if (pvlist == null || t.getPackageInfo() == null || pvlist.contains(t.getPackageInfo().getVID())) { 675 res.add(t.getResource()); 676 } 677 } 678 } 679 if (res.isEmpty() && pvlist != null && possibleMatches) { 680 return getSupplements(url, version, null); 681 } else { 682 return res; 683 } 684 } 685 686 public void clear() { 687 list.clear(); 688 map.clear(); 689 690 } 691 692 public List<CachedCanonicalResource<T>> getCachedList() { 693 return list; 694 } 695 696 public List<T> getList() { 697 List<T> res = new ArrayList<>(); 698 for (CachedCanonicalResource<T> t : list) { 699 if (!res.contains(t.getResource())) { 700 res.add(t.getResource()); 701 } 702 } 703 return res; 704 } 705 706 public List<T> getSortedList() { 707 List<T> res = getList(); 708 Collections.sort(res, new CanonicalListSorter()); 709 return res; 710 } 711 712 public Set<String> keys() { 713 return map.keySet(); 714 } 715 716 public boolean isEnforceUniqueId() { 717 return enforceUniqueId; 718 } 719 720 721 public void unload() { 722 for (CachedCanonicalResource<T> t : list) { 723 t.unload(); 724 } 725 726 } 727 728 729}