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