001package org.hl7.fhir.convertors.analytics;
002
003import java.io.File;
004import java.io.FileInputStream;
005import java.io.IOException;
006import java.util.*;
007
008import javax.xml.parsers.ParserConfigurationException;
009
010import org.hl7.fhir.exceptions.FHIRException;
011import org.hl7.fhir.r5.utils.EOperationOutcome;
012import org.hl7.fhir.utilities.FileUtilities;
013import org.hl7.fhir.utilities.Utilities;
014import org.hl7.fhir.utilities.filesystem.ManagedFileAccess;
015import org.hl7.fhir.utilities.http.HTTPResult;
016import org.hl7.fhir.utilities.http.ManagedWebAccess;
017import org.hl7.fhir.utilities.json.model.JsonArray;
018import org.hl7.fhir.utilities.json.model.JsonObject;
019import org.hl7.fhir.utilities.json.parser.JsonParser;
020import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
021import org.hl7.fhir.utilities.npm.NpmPackage;
022import org.hl7.fhir.utilities.npm.NpmPackage.PackagedResourceFile;
023import org.hl7.fhir.utilities.npm.PackageClient;
024import org.hl7.fhir.utilities.npm.PackageInfo;
025import org.hl7.fhir.utilities.npm.PackageServer;
026import org.hl7.fhir.utilities.xml.XMLUtil;
027import org.w3c.dom.Document;
028import org.w3c.dom.Element;
029import org.xml.sax.SAXException;
030
031public class PackageVisitor {
032
033  private PackageServer clientPackageServer = null;
034
035  public void setClientPackageServer(PackageServer packageServer) {
036    this.clientPackageServer = packageServer;
037  }
038  private List<PackageServer> cachePackageServers = null;
039  public void setCachePackageServers(List<PackageServer> packageServers) {
040    this.cachePackageServers = packageServers;
041  }
042
043  public static class PackageContext {
044    private String pid;
045    private NpmPackage npm;
046    private String version;
047    protected PackageContext(String pid, NpmPackage npm, String version) {
048      super();
049      this.pid = pid;
050      this.npm = npm;
051      this.version = version;
052    }
053    public String getPid() {
054      return pid;
055    }
056    public NpmPackage getNpm() {
057      return npm;
058    }
059    public String getVersion() {
060      return version;
061    }
062  }
063  
064  public interface IPackageVisitorProcessor {
065    public Object startPackage(PackageContext context) throws FHIRException, IOException, EOperationOutcome;
066    public void processResource(PackageContext context, Object clientContext, String type, String id, byte[] content) throws FHIRException, IOException, EOperationOutcome;
067    public void finishPackage(PackageContext context) throws FHIRException, IOException, EOperationOutcome;
068
069    public void alreadyVisited(String pid) throws FHIRException, IOException, EOperationOutcome;
070  }
071
072  private Set<String> resourceTypes = new HashSet<>();
073  private List<String> versions = new ArrayList<>();
074  private boolean corePackages;
075  private boolean oldVersions;
076  private boolean current;
077  private IPackageVisitorProcessor processor;
078  private FilesystemPackageCacheManager pcm;
079  private PackageClient pc;
080  private String cache;  
081  private int step;
082
083  public Set<String> getResourceTypes() {
084    return resourceTypes;
085  }
086
087  public void setResourceTypes(Set<String> resourceTypes) {
088    this.resourceTypes = resourceTypes;
089  }
090
091  public void setResourceTypes(String... resourceTypes) {
092    this.resourceTypes = new HashSet<String>();
093    for (String s : resourceTypes) {
094      this.resourceTypes.add(s);
095    }
096  }
097
098  public List<String> getVersions() {
099    return versions;
100  }
101
102  public void setVersions(List<String> versions) {
103    this.versions = versions;
104  }
105
106
107  public boolean isCurrent() {
108    return current;
109  }
110
111  public void setCurrent(boolean current) {
112    this.current = current;
113  }
114
115  public boolean isCorePackages() {
116    return corePackages;
117  }
118
119
120
121
122  public String getCache() {
123    return cache;
124  }
125
126  public void setCache(String cache) {
127    this.cache = cache;
128  }
129
130  public void setCorePackages(boolean corePackages) {
131    this.corePackages = corePackages;
132  }
133
134
135
136
137  public boolean isOldVersions() {
138    return oldVersions;
139  }
140
141
142
143
144  public void setOldVersions(boolean oldVersions) {
145    this.oldVersions = oldVersions;
146  }
147
148
149
150
151  public IPackageVisitorProcessor getProcessor() {
152    return processor;
153  }
154
155  public void setProcessor(IPackageVisitorProcessor processor) {
156    this.processor = processor;
157  }
158
159  public void visitPackages() throws IOException, ParserConfigurationException, SAXException, FHIRException, EOperationOutcome {
160    System.out.println("Finding packages");
161    pc = clientPackageServer == null
162      ? new PackageClient(PackageServer.primaryServer())
163      : new PackageClient(clientPackageServer);
164
165    pcm = cachePackageServers == null
166      ? new FilesystemPackageCacheManager.Builder().build()
167      : new FilesystemPackageCacheManager.Builder().withPackageServers(cachePackageServers).build();
168
169    Set<String> pidList = getAllPackages();
170
171    Map<String, String> cpidMap = getAllCIPackages();
172    Set<String> cpidSet = new HashSet<>();
173    System.out.println("Go: "+cpidMap.size()+" current packages");
174    int i = 0;
175    for (String s : cpidMap.keySet()) {
176      processCurrentPackage(cpidMap.get(s), s, cpidSet, i, cpidMap.size()); 
177      i++;
178    }
179
180    System.out.println("Go: "+pidList.size()+" published packages");
181    i = 0;
182    for (String pid : pidList) {  
183      if (pid != null) {
184        if (!cpidSet.contains(pid)) {
185          cpidSet.add(pid);
186          if (step == 0 || step == 3) {
187            List<String> vList = listVersions(pid);
188            if (oldVersions) {
189              for (String v : vList) {
190                processPackage(pid, v, i, pidList.size());          
191              }
192            } else if (vList.isEmpty()) {
193              System.out.println("No Packages for "+pid);
194            } else {
195              processPackage(pid, vList.get(vList.size() - 1), i, pidList.size());
196            }
197          }
198        } else {
199          processor.alreadyVisited(pid);
200        }
201        i++;
202      }    
203    }
204
205    if (step == 0 || step == 3) {
206      JsonObject json = JsonParser.parseObjectFromUrl("https://fhir.github.io/ig-registry/fhir-ig-list.json");
207      i = 0;
208      List<JsonObject> objects = json.getJsonObjects("guides");
209      for (JsonObject o : objects) {
210        String pid = o.asString("npm-name");
211        if (pid != null && !cpidSet.contains(pid)) {
212          cpidSet.add(pid);
213          List<String> vList = listVersions(pid);
214          if (oldVersions) {
215            for (String v : vList) {
216              processPackage(pid, v, i, objects.size());          
217            }
218          } else if (vList.isEmpty()) {
219            System.out.println("No Packages for "+pid);
220          } else {
221            processPackage(pid, vList.get(vList.size() - 1), i, objects.size());
222          }
223        }
224        i++;
225      }
226    }
227  }
228
229  private void processCurrentPackage(String url, String pid, Set<String> cpidSet, int i, int t) {
230    try {
231      cpidSet.add(pid);
232      if (step == 0 || (step == 1 && i < t/2) || (step == 2 && i >= t/2)) {
233        long ms1 = System.currentTimeMillis();
234        String[] p = url.split("\\/");
235        String repo = "https://build.fhir.org/ig/"+p[0]+"/"+p[1];
236        JsonObject manifest = JsonParser.parseObjectFromUrl(repo+"/package.manifest.json");
237        File co = ManagedFileAccess.file(Utilities.path(cache, pid+"."+manifest.asString("date")+".tgz"));
238        if (!co.exists()) {
239
240          HTTPResult res = ManagedWebAccess.get(Arrays.asList("web"), repo+"/package.tgz?nocache=" + System.currentTimeMillis());
241          res.checkThrowException();
242          FileUtilities.bytesToFile(res.getContent(), co);
243        }
244        NpmPackage npm = NpmPackage.fromPackage(ManagedFileAccess.inStream(co));          
245        String fv = npm.fhirVersion();
246        long ms2 = System.currentTimeMillis();
247
248        if (corePackages || !corePackage(npm)) {
249          if (fv != null && (versions.isEmpty() || versions.contains(fv))) {
250            PackageContext ctxt = new PackageContext(pid+"#current", npm, fv);
251            boolean ok = false;
252            Object context = null;
253            try {
254              context = processor.startPackage(ctxt);
255              ok = true;
256            } catch (Exception e) {
257              System.out.println("####### Error loading "+pid+"#current["+fv+"]: ####### "+e.getMessage());
258              //                e.printStackTrace();
259            }
260            if (ok) {
261              int c = 0;
262              for (String type : resourceTypes) {
263                for (String s : npm.listResources(type)) {
264                  c++;
265                  try {
266                    processor.processResource(ctxt, context, type, s, FileUtilities.streamToBytes(npm.load("package", s)));
267                  } catch (Exception e) {
268                    System.out.println("####### Error loading "+pid+"#current["+fv+"]/"+type+" ####### "+e.getMessage());
269                    //                e.printStackTrace();
270                  }
271                }
272              }
273              processor.finishPackage(ctxt);
274              System.out.println("Processed: "+pid+"#current: "+c+" resources ("+i+" of "+t+", "+(ms2-ms1)+"/"+(System.currentTimeMillis()-ms2)+"ms)");
275            }
276          } else {
277            System.out.println("Ignored: "+pid+"#current: no version");            
278          }
279        }
280      }
281    } catch (Exception e) {      
282      System.out.println("Unable to process: "+pid+"#current: "+e.getMessage());      
283    }
284  }
285
286  private Map<String, String> getAllCIPackages() throws IOException {
287    System.out.println("Fetch https://build.fhir.org/ig/qas.json");
288    Map<String, String> res = new HashMap<>();
289    if (current) {
290      JsonArray json = (JsonArray) JsonParser.parseFromUrl("https://build.fhir.org/ig/qas.json");
291      for (JsonObject o  : json.asJsonObjects()) {
292        String url = o.asString("repo");
293        String pid = o.asString("package-id");
294        if (url.contains("/branches/master") || url.contains("/branches/main") ) {
295          if (!res.containsKey(pid)) {
296            res.put(pid, url);
297          } else if (!url.equals(res.get(pid))) {
298            System.out.println("Ignore "+url+" already encountered "+pid +" @ "+res.get(pid));
299          }
300        }
301      }
302    }
303    return res;
304  }
305
306  private List<String> listVersions(String pid) throws IOException {
307    List<String> list = new ArrayList<>();
308    if (pid !=null) {
309      for (PackageInfo i : pc.getVersions(pid)) {
310        list.add(i.getVersion());
311      }    
312    }
313    return list;
314  }
315
316  private Set<String> getAllPackages() throws IOException, ParserConfigurationException, SAXException {
317    Set<String> list = new HashSet<>();
318    for (PackageInfo i : pc.search(null, null, null, false)) {
319      list.add(i.getId());
320    }    
321    JsonObject json = JsonParser.parseObjectFromUrl("https://fhir.github.io/ig-registry/fhir-ig-list.json");
322    for (JsonObject ig : json.getJsonObjects("guides")) {
323      list.add(ig.asString("npm-name"));
324    }
325    json = JsonParser.parseObjectFromUrl("https://fhir.github.io/ig-registry/package-feeds.json");
326    for (JsonObject feed : json.getJsonObjects("feeds")) {
327      processFeed(list, feed.asString("url"));
328    }
329
330    return list;
331  }
332
333  private void processFeed(Set<String> list, String str) throws IOException, ParserConfigurationException, SAXException {
334    System.out.println("Feed "+str);
335    try {
336
337      HTTPResult res = ManagedWebAccess.get(Arrays.asList("web"), str+"?nocache=" + System.currentTimeMillis());
338      res.checkThrowException();
339      Document xml = XMLUtil.parseToDom(res.getContent());
340      for (Element channel : XMLUtil.getNamedChildren(xml.getDocumentElement(), "channel")) {
341        for (Element item : XMLUtil.getNamedChildren(channel, "item")) {
342          String pid = XMLUtil.getNamedChildText(item, "title");
343          if (pid != null && pid.contains("#")) {
344            list.add(pid.substring(0, pid.indexOf("#")));
345          }
346        }
347      }
348    } catch (Exception e) {
349      System.out.println("   "+e.getMessage());
350    }
351  }
352
353
354  private void processPackage(String pid, String v, int i, int t) throws IOException, FHIRException, EOperationOutcome {
355    NpmPackage npm = null;
356    String fv = null;
357    try {
358      npm = pcm.loadPackage(pid, v);
359    } catch (Throwable e) {
360      System.out.println("Unable to load package: "+pid+"#"+v+": "+e.getMessage());
361      return;
362    }
363
364    try {
365      fv = npm.fhirVersion();
366    } catch (Throwable e) {
367      System.out.println("Unable to identify package FHIR version:: "+pid+"#"+v+": "+e.getMessage());
368    }
369    if (corePackages || !corePackage(npm)) {
370      PackageContext ctxt = new PackageContext(pid+"#"+v, npm, fv);
371      boolean ok = false;
372      Object context = null;
373      try {
374        context = processor.startPackage(ctxt);
375        ok = true;
376      } catch (Exception e) {
377        System.out.println("####### Error loading package  "+pid+"#"+v +"["+fv+"]: "+e.getMessage());
378        e.printStackTrace();
379      }
380      if (ok) {
381        int c = 0;
382        if (fv != null && (versions.isEmpty() || versions.contains(fv))) {
383          for (PackagedResourceFile p : npm.listAllResources(resourceTypes)) {
384            c++;
385            try {
386              processor.processResource(ctxt, context, p.getResourceType(), p.getFilename(), FileUtilities.streamToBytes(npm.load(p.getFolder(), p.getFilename())));
387            } catch (Exception e) {
388              System.out.println("####### Error loading "+pid+"#"+v +"["+fv+"]/"+p.getResourceType()+" ####### "+e.getMessage());
389              e.printStackTrace();
390            }
391          }
392        }    
393        processor.finishPackage(ctxt);
394        System.out.println("Processed: "+pid+"#"+v+": "+c+" resources ("+i+" of "+t+")");  
395      }
396    }
397  }
398
399  private boolean corePackage(NpmPackage npm) {
400    return npm != null && !Utilities.noString(npm.name()) && (
401        npm.name().startsWith("hl7.terminology") || 
402        npm.name().startsWith("hl7.fhir.core") || 
403        npm.name().startsWith("hl7.fhir.r2.") || 
404        npm.name().startsWith("hl7.fhir.r2b.") || 
405        npm.name().startsWith("hl7.fhir.r3.") || 
406        npm.name().startsWith("hl7.fhir.r4.") || 
407        npm.name().startsWith("hl7.fhir.r4b.") || 
408        npm.name().startsWith("hl7.fhir.r5."));
409  }
410
411  public int getStep() {
412    return step;
413  }
414
415  public void setStep(int step) {
416    this.step = step;
417  }
418
419}