001package org.hl7.fhir.r4b.context;
002
003import java.util.ArrayList;
004import java.util.Collections;
005import java.util.Comparator;
006import java.util.HashMap;
007import java.util.List;
008import java.util.Map;
009import java.util.Set;
010import java.util.UUID;
011
012import org.hl7.fhir.exceptions.FHIRException;
013import org.hl7.fhir.r4b.context.CanonicalResourceManager.CanonicalResourceProxy;
014import org.hl7.fhir.r4b.context.IWorkerContext.PackageVersion;
015import org.hl7.fhir.r4b.model.CanonicalResource;
016import org.hl7.fhir.r4b.model.CodeSystem;
017import org.hl7.fhir.r4b.model.DomainResource;
018import org.hl7.fhir.r4b.terminologies.CodeSystemUtilities;
019import org.hl7.fhir.utilities.VersionUtilities;
020
021/**
022 * 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
023 * note, though, that not all resources have semver versions
024 * 
025 * @author graha
026 *
027 */
028
029public class CanonicalResourceManager<T extends CanonicalResource> {
030
031  public static abstract class CanonicalResourceProxy {
032    private String type;
033    private String id;
034    private String url;
035    private String version;
036    private CanonicalResource resource;
037    
038    public CanonicalResourceProxy(String type, String id, String url, String version) {
039      super();
040      this.type = type;
041      this.id = id;
042      this.url = url;
043      this.version = version;
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 CanonicalResource getResource() throws FHIRException {
075      if (resource == null) {
076        resource = loadResource();
077        if (resource instanceof CodeSystem) {
078          CodeSystemUtilities.crossLinkCodeSystem((CodeSystem) resource);
079        }
080      }
081      return resource;
082    }
083
084    public void setResource(CanonicalResource resource) {
085      this.resource = resource;
086    }
087
088    public abstract CanonicalResource loadResource() throws FHIRException;
089
090    @Override
091    public String toString() {
092      return type+"/"+id+": "+url+"|"+version;
093    }      
094  }
095
096  public class CanonicalListSorter implements Comparator<CanonicalResource> {
097
098    @Override
099    public int compare(CanonicalResource arg0, CanonicalResource arg1) {
100      String u0 = arg0.getUrl();
101      String u1 = arg1.getUrl();
102      return u0.compareTo(u1);
103    }
104  }
105
106  private class CachedCanonicalResource<T1 extends CanonicalResource> {
107    private T1 resource;
108    private CanonicalResourceProxy proxy;
109    private PackageVersion packageInfo;
110    
111    public CachedCanonicalResource(T1 resource, PackageVersion packageInfo) {
112      super();
113      this.resource = resource;
114      this.packageInfo = packageInfo;
115    }
116    
117    public CachedCanonicalResource(CanonicalResourceProxy proxy, PackageVersion packageInfo) {
118      super();
119      this.proxy = proxy;
120      this.packageInfo = packageInfo;
121    }
122    
123    public T1 getResource() {
124      if (resource == null) {
125        @SuppressWarnings("unchecked")
126        T1 res = (T1) proxy.getResource();
127        synchronized (this) {
128          resource = res;
129        }
130        proxy = null;
131      }
132      return resource;
133    }
134    
135    public PackageVersion getPackageInfo() {
136      return packageInfo;
137    }
138    public String getUrl() {
139      return resource != null ? resource.getUrl() : proxy.getUrl();
140    }
141    public String getId() {
142      return resource != null ? resource.getId() : proxy.getId();
143    }
144    public String getVersion() {
145      return resource != null ? resource.getVersion() : proxy.getVersion();
146    }
147    public boolean hasVersion() {
148      return resource != null ? resource.hasVersion() : proxy.getVersion() != null;
149    }
150    
151    @Override
152    public String toString() {
153      return resource != null ? resource.fhirType()+"/"+resource.getId()+": "+resource.getUrl()+"|"+resource.getVersion() : proxy.toString();
154    }  
155
156  }
157
158  public class MetadataResourceVersionComparator<T1 extends CachedCanonicalResource<T>> implements Comparator<T1> {
159    @Override
160    public int compare(T1 arg1, T1 arg2) {
161      String v1 = arg1.getVersion();
162      String v2 = arg2.getVersion();
163      if (v1 == null && v2 == null) {
164        return Integer.compare(list.indexOf(arg1), list.indexOf(arg2)); // retain original order
165      } else if (v1 == null) {
166        return -1;
167      } else if (v2 == null) {
168        return 1;
169      } else {
170        String mm1 = VersionUtilities.getMajMin(v1);
171        String mm2 = VersionUtilities.getMajMin(v2);
172        if (mm1 == null || mm2 == null) {
173          return v1.compareTo(v2);
174        } else {
175          return mm1.compareTo(mm2);
176        }
177      }
178    }
179  }
180
181  private boolean enforceUniqueId; 
182  private List<CachedCanonicalResource<T>> list = new ArrayList<>();
183  private Map<String, CachedCanonicalResource<T>> map = new HashMap<>();
184  
185  
186  public CanonicalResourceManager(boolean enforceUniqueId) {
187    super();
188    this.enforceUniqueId = enforceUniqueId;
189  }
190
191  public void copy(CanonicalResourceManager<T> source) {
192    list.clear();
193    map.clear();
194    list.addAll(source.list);
195    map.putAll(source.map);
196  }
197  
198  public void register(CanonicalResourceProxy r, PackageVersion packgeInfo) {
199    if (!r.hasId()) {
200      throw new FHIRException("An id is required for a deferred load resource");
201    }
202    CanonicalResourceManager<T>.CachedCanonicalResource<T> cr = new CachedCanonicalResource<T>(r, packgeInfo);
203    see(cr);
204  }
205
206  public void see(T r, PackageVersion packgeInfo) {
207    if (r != null) {
208      if (!r.hasId()) {
209        r.setId(UUID.randomUUID().toString());
210      }
211      CanonicalResourceManager<T>.CachedCanonicalResource<T> cr = new CachedCanonicalResource<T>(r, packgeInfo);
212      see(cr);
213    }
214  }
215
216  public void see(CachedCanonicalResource<T> cr) {
217    // ignore UTG NUCC erroneous code system
218    if (cr.getPackageInfo() != null && cr.getPackageInfo().getId() != null && cr.getPackageInfo().getId().startsWith("hl7.terminology") && "http://nucc.org/provider-taxonomy".equals(cr.getUrl())) {
219      return;
220    }
221        
222    if (enforceUniqueId && map.containsKey(cr.getId())) {
223      drop(cr.getId());      
224    }
225    
226    // special case logic for UTG support prior to version 5
227    if (cr.getPackageInfo() != null && cr.getPackageInfo().getId().startsWith("hl7.terminology")) {
228      List<CachedCanonicalResource<T>> toDrop = new ArrayList<>();
229      for (CachedCanonicalResource<T> n : list) {
230        if (n.getUrl() != null && n.getUrl().equals(cr.getUrl()) && isBasePackage(n.getPackageInfo())) {
231          toDrop.add(n);
232        }
233      }
234      for (CachedCanonicalResource<T> n : toDrop) {
235        drop(n.getId());
236      }
237    }
238    CachedCanonicalResource<T> existing = cr.hasVersion() ? map.get(cr.getUrl()+"|"+cr.getVersion()) : map.get(cr.getUrl()+"|#0");
239    if (map.get(cr.getUrl()) != null && (cr.getPackageInfo() != null && cr.getPackageInfo().isExamplesPackage())) {
240      return;
241    }
242    if (existing != null) {
243      list.remove(existing);
244    }
245    
246    list.add(cr);
247    map.put(cr.getId(), cr); // we do this so we can drop by id
248    map.put(cr.getUrl(), cr);
249
250    if (cr.getUrl() != null) {
251      // first, this is the correct reosurce for this version (if it has a version)
252      if (cr.hasVersion()) {
253        map.put(cr.getUrl()+"|"+cr.getVersion(), cr);
254      } else {
255        map.put(cr.getUrl()+"|#0", cr);
256      }
257      updateList(cr.getUrl(), cr.getVersion());
258    }
259  }
260
261  private boolean isBasePackage(PackageVersion packageInfo) {
262    return packageInfo == null ? false : VersionUtilities.isCorePackage(packageInfo.getId());
263  }
264
265  private void updateList(String url, String version) {
266    List<CachedCanonicalResource<T>> rl = new ArrayList<>();
267    for (CachedCanonicalResource<T> t : list) {
268      if (url.equals(t.getUrl()) && !rl.contains(t)) {
269        rl.add(t);
270      }
271    }
272    if (rl.size() > 0) {
273      // sort by version as much as we are able
274      Collections.sort(rl, new MetadataResourceVersionComparator<CachedCanonicalResource<T>>());
275      // the current is the latest
276      map.put(url, rl.get(rl.size()-1));
277      // now, also, the latest for major/minor
278      if (version != null) {
279        CachedCanonicalResource<T> latest = null;
280        for (CachedCanonicalResource<T> t : rl) {
281          if (VersionUtilities.versionsCompatible(t.getVersion(), version)) {
282            latest = t;
283          }
284        }
285        if (latest != null) { // might be null if it's not using semver
286          String lv = VersionUtilities.getMajMin(latest.getVersion());
287          if (lv != null && !lv.equals(version))
288            map.put(url+"|"+lv, rl.get(rl.size()-1));
289        }
290      }
291    }
292  }
293 
294
295  public T get(String url) {
296    return map.containsKey(url) ? map.get(url).getResource() : null;
297  }
298  
299  public PackageVersion getPackageInfo(String system, String version) {
300    if (version == null) {
301      return map.containsKey(system) ? map.get(system).getPackageInfo() : null;
302    } else {
303      if (map.containsKey(system+"|"+version))
304        return map.get(system+"|"+version).getPackageInfo();
305      String mm = VersionUtilities.getMajMin(version);
306      if (mm != null && map.containsKey(system+"|"+mm))
307        return map.get(system+"|"+mm).getPackageInfo();
308      else
309        return null;
310    }
311  }
312  
313  public boolean has(String url) {
314    return map.containsKey(url);
315  }
316  
317  public T get(String system, String version) {
318    if (version == null) {
319      return get(system);
320    } else {
321      if (map.containsKey(system+"|"+version))
322        return map.get(system+"|"+version).getResource();
323      String mm = VersionUtilities.getMajMin(version);
324      if (mm != null && map.containsKey(system+"|"+mm))
325        return map.get(system+"|"+mm).getResource();
326      else
327        return null;
328    }
329  }
330  
331  public boolean has(String system, String version) {
332    if (map.containsKey(system+"|"+version))
333      return true;
334    String mm = VersionUtilities.getMajMin(version);
335    if (mm != null)
336      return map.containsKey(system+"|"+mm);
337    else
338      return false;
339  }
340  
341  public int size() {
342    return list.size();
343  }
344  
345  public void drop(String id) {
346    CachedCanonicalResource<T> res = null;
347    do {
348      res = null;
349      for (CachedCanonicalResource<T> t : list) {
350        if (t.getId().equals(id)) {
351          res = t;
352        }
353      }
354      if (res != null) {
355        list.remove(res);
356        map.remove(id);
357        map.remove(res.getUrl());
358        if (res.hasVersion()) {
359          map.remove(res.getUrl()+"|"+res.getVersion());
360          String mm = VersionUtilities.getMajMin(res.getVersion());
361          if (mm != null) {
362            map.remove(res.getUrl()+"|"+mm);
363          }
364        }
365        updateList(res.getUrl(), res.getVersion()); 
366      }
367    } while (res != null);
368  }
369  
370  
371  public void listAll(List<T> result) {
372    for (CachedCanonicalResource<T>  t : list) {
373      result.add(t.getResource()); 
374    }
375  }
376
377  public void listAllM(List<CanonicalResource> result) {
378    for (CachedCanonicalResource<T>  t : list) {
379      result.add(t.getResource()); 
380    }
381  }
382
383  public void clear() {
384    list.clear();
385    map.clear();
386    
387  }
388
389  public List<T> getList() {
390    List<T> res = new ArrayList<>();
391    for (CachedCanonicalResource<T> t : list) {
392      if (!res.contains(t.getResource())) {
393        res.add(t.getResource());
394      }
395    }
396    return res;
397  }
398
399  public List<T> getSortedList() {
400    List<T> res = getList();
401    Collections.sort(res, new CanonicalListSorter());
402    return res;
403  }
404
405  public Set<String> keys() {
406    return map.keySet();
407  }
408
409  public boolean isEnforceUniqueId() {
410    return enforceUniqueId;
411  }
412  
413}