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