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