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