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