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}