001package org.hl7.fhir.r5.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.r5.context.CanonicalResourceManager.CanonicalResourceProxy;
014import org.hl7.fhir.r5.context.IWorkerContext.PackageVersion;
015import org.hl7.fhir.r5.model.CanonicalResource;
016import org.hl7.fhir.r5.model.CodeSystem;
017import org.hl7.fhir.r5.model.DomainResource;
018import org.hl7.fhir.r5.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        if (res == null) {
128          throw new Error("Proxy loading a resource from "+packageInfo+" failed and returned null");
129        }
130        synchronized (this) {
131          resource = res;
132        }
133        proxy = null;
134      }
135      return resource;
136    }
137    
138    public PackageVersion getPackageInfo() {
139      return packageInfo;
140    }
141    public String getUrl() {
142      return resource != null ? resource.getUrl() : proxy.getUrl();
143    }
144    public String getId() {
145      return resource != null ? resource.getId() : proxy.getId();
146    }
147    public String getVersion() {
148      return resource != null ? resource.getVersion() : proxy.getVersion();
149    }
150    public boolean hasVersion() {
151      return resource != null ? resource.hasVersion() : proxy.getVersion() != null;
152    }
153    
154    @Override
155    public String toString() {
156      return resource != null ? resource.fhirType()+"/"+resource.getId()+": "+resource.getUrl()+"|"+resource.getVersion() : proxy.toString();
157    }  
158
159  }
160
161  public class MetadataResourceVersionComparator<T1 extends CachedCanonicalResource<T>> implements Comparator<T1> {
162    @Override
163    public int compare(T1 arg1, T1 arg2) {
164      String v1 = arg1.getVersion();
165      String v2 = arg2.getVersion();
166      if (v1 == null && v2 == null) {
167        return Integer.compare(list.indexOf(arg1), list.indexOf(arg2)); // retain original order
168      } else if (v1 == null) {
169        return -1;
170      } else if (v2 == null) {
171        return 1;
172      } else {
173        String mm1 = VersionUtilities.getMajMin(v1);
174        String mm2 = VersionUtilities.getMajMin(v2);
175        if (mm1 == null || mm2 == null) {
176          return v1.compareTo(v2);
177        } else {
178          return mm1.compareTo(mm2);
179        }
180      }
181    }
182  }
183
184  private boolean enforceUniqueId; 
185  private List<CachedCanonicalResource<T>> list = new ArrayList<>();
186  private Map<String, CachedCanonicalResource<T>> map = new HashMap<>();
187  
188  
189  public CanonicalResourceManager(boolean enforceUniqueId) {
190    super();
191    this.enforceUniqueId = enforceUniqueId;
192  }
193
194  public void copy(CanonicalResourceManager<T> source) {
195    list.clear();
196    map.clear();
197    list.addAll(source.list);
198    map.putAll(source.map);
199  }
200  
201  public void register(CanonicalResourceProxy r, PackageVersion packgeInfo) {
202    if (!r.hasId()) {
203      throw new FHIRException("An id is required for a deferred load resource");
204    }
205    CanonicalResourceManager<T>.CachedCanonicalResource<T> cr = new CachedCanonicalResource<T>(r, packgeInfo);
206    see(cr);
207  }
208
209  public void see(T r, PackageVersion packgeInfo) {
210    if (r != null) {
211      if (!r.hasId()) {
212        r.setId(UUID.randomUUID().toString());
213      }
214      CanonicalResourceManager<T>.CachedCanonicalResource<T> cr = new CachedCanonicalResource<T>(r, packgeInfo);
215      see(cr);
216    }
217  }
218
219  public void see(CachedCanonicalResource<T> cr) {
220    // ignore UTG NUCC erroneous code system
221    if (cr.getPackageInfo() != null && cr.getPackageInfo().getId() != null && cr.getPackageInfo().getId().startsWith("hl7.terminology") && "http://nucc.org/provider-taxonomy".equals(cr.getUrl())) {
222      return;
223    }
224        
225    if (enforceUniqueId && map.containsKey(cr.getId())) {
226      drop(cr.getId());      
227    }
228    
229    // special case logic for UTG support prior to version 5
230    if (cr.getPackageInfo() != null && cr.getPackageInfo().getId().startsWith("hl7.terminology")) {
231      List<CachedCanonicalResource<T>> toDrop = new ArrayList<>();
232      for (CachedCanonicalResource<T> n : list) {
233        if (n.getUrl() != null && n.getUrl().equals(cr.getUrl()) && isBasePackage(n.getPackageInfo())) {
234          toDrop.add(n);
235        }
236      }
237      for (CachedCanonicalResource<T> n : toDrop) {
238        drop(n.getId());
239      }
240    }
241    CachedCanonicalResource<T> existing = cr.hasVersion() ? map.get(cr.getUrl()+"|"+cr.getVersion()) : map.get(cr.getUrl()+"|#0");
242    if (map.get(cr.getUrl()) != null && (cr.getPackageInfo() != null && cr.getPackageInfo().isExamplesPackage())) {
243      return;
244    }
245    if (existing != null) {
246      list.remove(existing);
247    }
248    
249    list.add(cr);
250    map.put(cr.getId(), cr); // we do this so we can drop by id
251    map.put(cr.getUrl(), cr);
252
253    if (cr.getUrl() != null) {
254      // first, this is the correct reosurce for this version (if it has a version)
255      if (cr.hasVersion()) {
256        map.put(cr.getUrl()+"|"+cr.getVersion(), cr);
257      } else {
258        map.put(cr.getUrl()+"|#0", cr);
259      }
260      updateList(cr.getUrl(), cr.getVersion());
261    }
262  }
263
264  private boolean isBasePackage(PackageVersion packageInfo) {
265    return packageInfo == null ? false : VersionUtilities.isCorePackage(packageInfo.getId());
266  }
267
268  private void updateList(String url, String version) {
269    List<CachedCanonicalResource<T>> rl = new ArrayList<>();
270    for (CachedCanonicalResource<T> t : list) {
271      if (url.equals(t.getUrl()) && !rl.contains(t)) {
272        rl.add(t);
273      }
274    }
275    if (rl.size() > 0) {
276      // sort by version as much as we are able
277      Collections.sort(rl, new MetadataResourceVersionComparator<CachedCanonicalResource<T>>());
278      // the current is the latest
279      map.put(url, rl.get(rl.size()-1));
280      // now, also, the latest for major/minor
281      if (version != null) {
282        CachedCanonicalResource<T> latest = null;
283        for (CachedCanonicalResource<T> t : rl) {
284          if (VersionUtilities.versionsCompatible(t.getVersion(), version)) {
285            latest = t;
286          }
287        }
288        if (latest != null) { // might be null if it's not using semver
289          String lv = VersionUtilities.getMajMin(latest.getVersion());
290          if (lv != null && !lv.equals(version))
291            map.put(url+"|"+lv, rl.get(rl.size()-1));
292        }
293      }
294    }
295  }
296 
297
298  public T get(String url) {
299    return map.containsKey(url) ? map.get(url).getResource() : null;
300  }
301  
302  public PackageVersion getPackageInfo(String system, String version) {
303    if (version == null) {
304      return map.containsKey(system) ? map.get(system).getPackageInfo() : null;
305    } else {
306      if (map.containsKey(system+"|"+version))
307        return map.get(system+"|"+version).getPackageInfo();
308      String mm = VersionUtilities.getMajMin(version);
309      if (mm != null && map.containsKey(system+"|"+mm))
310        return map.get(system+"|"+mm).getPackageInfo();
311      else
312        return null;
313    }
314  }
315  
316  public boolean has(String url) {
317    return map.containsKey(url);
318  }
319  
320  public T get(String system, String version) {
321    if (version == null) {
322      return get(system);
323    } else {
324      if (map.containsKey(system+"|"+version))
325        return map.get(system+"|"+version).getResource();
326      String mm = VersionUtilities.getMajMin(version);
327      if (mm != null && map.containsKey(system+"|"+mm))
328        return map.get(system+"|"+mm).getResource();
329      else
330        return null;
331    }
332  }
333  
334  public boolean has(String system, String version) {
335    if (map.containsKey(system+"|"+version))
336      return true;
337    String mm = VersionUtilities.getMajMin(version);
338    if (mm != null)
339      return map.containsKey(system+"|"+mm);
340    else
341      return false;
342  }
343  
344  public int size() {
345    return list.size();
346  }
347  
348  public void drop(String id) {
349    CachedCanonicalResource<T> res = null;
350    do {
351      res = null;
352      for (CachedCanonicalResource<T> t : list) {
353        if (t.getId().equals(id)) {
354          res = t;
355        }
356      }
357      if (res != null) {
358        list.remove(res);
359        map.remove(id);
360        map.remove(res.getUrl());
361        if (res.hasVersion()) {
362          map.remove(res.getUrl()+"|"+res.getVersion());
363          String mm = VersionUtilities.getMajMin(res.getVersion());
364          if (mm != null) {
365            map.remove(res.getUrl()+"|"+mm);
366          }
367        }
368        updateList(res.getUrl(), res.getVersion()); 
369      }
370    } while (res != null);
371  }
372  
373  
374  public void listAll(List<T> result) {
375    for (CachedCanonicalResource<T>  t : list) {
376      result.add(t.getResource()); 
377    }
378  }
379
380  public void listAllM(List<CanonicalResource> result) {
381    for (CachedCanonicalResource<T>  t : list) {
382      result.add(t.getResource()); 
383    }
384  }
385
386  public void clear() {
387    list.clear();
388    map.clear();
389    
390  }
391
392  public List<T> getList() {
393    List<T> res = new ArrayList<>();
394    for (CachedCanonicalResource<T> t : list) {
395      if (!res.contains(t.getResource())) {
396        res.add(t.getResource());
397      }
398    }
399    return res;
400  }
401
402  public List<T> getSortedList() {
403    List<T> res = getList();
404    Collections.sort(res, new CanonicalListSorter());
405    return res;
406  }
407
408  public Set<String> keys() {
409    return map.keySet();
410  }
411
412  public boolean isEnforceUniqueId() {
413    return enforceUniqueId;
414  }
415  
416}