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