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