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