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