001package org.hl7.fhir.utilities.cache; 002 003/*- 004 * #%L 005 * org.hl7.fhir.utilities 006 * %% 007 * Copyright (C) 2014 - 2019 Health Level 7 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023 024import java.io.BufferedOutputStream; 025import java.io.ByteArrayOutputStream; 026import java.io.File; 027import java.io.FileInputStream; 028import java.io.IOException; 029import java.io.InputStream; 030import java.net.HttpURLConnection; 031import java.net.URL; 032import java.net.URLConnection; 033import java.sql.Timestamp; 034import java.text.ParseException; 035import java.text.SimpleDateFormat; 036import java.time.Instant; 037import java.util.ArrayList; 038import java.util.Collections; 039import java.util.Comparator; 040import java.util.Date; 041import java.util.HashMap; 042import java.util.List; 043import java.util.Locale; 044import java.util.Map; 045import java.util.Map.Entry; 046 047import org.apache.commons.compress.archivers.tar.TarArchiveEntry; 048import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; 049import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; 050import org.apache.commons.io.FileUtils; 051import org.hl7.fhir.exceptions.FHIRException; 052import org.hl7.fhir.utilities.IniFile; 053import org.hl7.fhir.utilities.TextFile; 054import org.hl7.fhir.utilities.Utilities; 055import org.hl7.fhir.utilities.cache.NpmPackage.NpmPackageFolder; 056import org.hl7.fhir.utilities.cache.PackageCacheManager.BuildRecord; 057import org.hl7.fhir.utilities.cache.PackageCacheManager.BuildRecordSorter; 058import org.hl7.fhir.utilities.json.JSONUtil; 059 060import com.google.gson.GsonBuilder; 061import com.google.gson.JsonArray; 062import com.google.gson.JsonElement; 063import com.google.gson.JsonObject; 064 065/** 066 * Package cache manager 067 * 068 * API: 069 * 070 * constructor 071 * getPackageUrl 072 * getPackageId 073 * findPackageCache 074 * addPackageToCache 075 * 076 * @author Grahame Grieve 077 * 078 */ 079public class PackageCacheManager { 080 081 public class BuildRecordSorter implements Comparator<BuildRecord> { 082 083 @Override 084 public int compare(BuildRecord arg0, BuildRecord arg1) { 085 return arg1.date.compareTo(arg0.date); 086 } 087 } 088 089 public class BuildRecord { 090 091 private String url; 092 private String packageId; 093 private String repo; 094 private Date date; 095 public BuildRecord(String url, String packageId, String repo, Date date) { 096 super(); 097 this.url = url; 098 this.packageId = packageId; 099 this.repo = repo; 100 this.date = date; 101 } 102 public String getUrl() { 103 return url; 104 } 105 public String getPackageId() { 106 return packageId; 107 } 108 public String getRepo() { 109 return repo; 110 } 111 public Date getDate() { 112 return date; 113 } 114 115 116 } 117 118 /** if you don't provide and implementation of this interface, the PackageCacheManager will use the web directly. 119 * 120 * You can use this interface to 121 * @author graha 122 * 123 */ 124 public interface INetworkServices { 125 126 InputStream resolvePackage(String packageId, String version); 127 } 128 129 public class VersionHistory { 130 private String id; 131 private String canonical; 132 private String current; 133 private Map<String, String> versions = new HashMap<>(); 134 public String getCanonical() { 135 return canonical; 136 } 137 public String getCurrent() { 138 return current; 139 } 140 public Map<String, String> getVersions() { 141 return versions; 142 } 143 public String getId() { 144 return id; 145 } 146 } 147 148 149 public class PackageEntry { 150 151 private byte[] bytes; 152 private String name; 153 154 public PackageEntry(String name) { 155 this.name = name; 156 } 157 158 public PackageEntry(String name, byte[] bytes) { 159 this.name = name; 160 this.bytes = bytes; 161 } 162 } 163 164 public static final String PACKAGE_REGEX = "^[a-z][a-z0-9\\_\\-]*(\\.[a-z0-9\\_\\-]+)+$"; 165 public static final String PACKAGE_VERSION_REGEX = "^[a-z][a-z0-9\\_\\-]*(\\.[a-z0-9\\_\\-]+)+\\#[a-z0-9\\-\\_]+(\\.[a-z0-9\\-\\_]+)*$"; 166 167 private static final int BUFFER_SIZE = 1024; 168 private static final String CACHE_VERSION = "3"; // second version - see wiki page 169 private static final int ANALYSIS_VERSION = 2; 170 171 private String cacheFolder; 172 private boolean buildLoaded; 173 private JsonArray buildInfo; 174 private boolean progress = true; 175 private List<NpmPackage> temporaryPackages = new ArrayList<NpmPackage>(); 176 private Map<String, String> ciList = new HashMap<String, String>(); 177 private List<String> allUrls; 178 private Map<String, VersionHistory> historyCache = new HashMap<>(); 179 180 public PackageCacheManager(boolean userMode, int toolsVersion) throws IOException { 181 if (userMode) 182 cacheFolder = Utilities.path(System.getProperty("user.home"), ".fhir", "packages"); 183 else 184 cacheFolder = Utilities.path("var", "lib", ".fhir", "packages"); 185 if (!(new File(cacheFolder).exists())) 186 Utilities.createDirectory(cacheFolder); 187 if (!(new File(Utilities.path(cacheFolder, "packages.ini")).exists())) 188 TextFile.stringToFile("[cache]\r\nversion="+CACHE_VERSION+"\r\n\r\n[urls]\r\n\r\n[local]\r\n\r\n", Utilities.path(cacheFolder, "packages.ini"), false); 189 IniFile ini = new IniFile(Utilities.path(cacheFolder, "packages.ini")); 190 boolean save = false; 191 String v = ini.getStringProperty("cache", "version"); 192 if (!CACHE_VERSION.equals(v)) { 193 clearCache(); 194 ini.setStringProperty("cache", "version", CACHE_VERSION, null); 195 save = true; 196 } 197 save = initUrlMaps(ini, save); 198 if (save) { 199 if (!CACHE_VERSION.equals(ini.getStringProperty("cache", "version"))) { 200 throw new Error("what?"); 201 } 202 ini.save(); 203 } 204 } 205 206 public static String userDir() throws IOException { 207 return Utilities.path(System.getProperty("user.home"), ".fhir", "packages"); 208 } 209 210 // ========================= Utilities ============================================================================ 211 212 private List<String> sorted(String[] keys) { 213 List<String> names = new ArrayList<String>(); 214 for (String s : keys) 215 names.add(s); 216 Collections.sort(names); 217 return names; 218 } 219 220 private NpmPackage loadPackageInfo(String path) throws IOException { 221 NpmPackage pi = NpmPackage.fromFolder(path); 222 return pi; 223 } 224 225 public String getFolder() { 226 return cacheFolder; 227 } 228 229 230 231 232 233 private NpmPackage checkCurrency(String id, NpmPackage p) { 234 // special case: current versions roll over, and we have to check their currency 235 try { 236 String url = ciList.get(id); 237 JsonObject json = fetchJson(Utilities.pathURL(url, "package.manifest.json")); 238 String currDate = JSONUtil.str(json, "date"); 239 String packDate = p.date(); 240 if (!currDate.equals(packDate)) 241 return null; // nup, we need a new copy 242 return p; 243 } catch (Exception e) { 244 return p; 245 } 246 } 247 248 private JsonObject fetchJson(String source) throws IOException { 249 URL url = new URL(source); 250 URLConnection c = url.openConnection(); 251 return (JsonObject) new com.google.gson.JsonParser().parse(TextFile.streamToString(c.getInputStream())); 252 } 253 254 private InputStream fetchFromUrlSpecific(String source, boolean optional) throws FHIRException { 255 try { 256 URL url = new URL(source); 257 URLConnection c = url.openConnection(); 258 return c.getInputStream(); 259 } catch (Exception e) { 260 if (optional) 261 return null; 262 else 263 throw new FHIRException(e.getMessage(), e); 264 } 265 } 266 267 // ========================= Full Cache Management ============================================================================ 268 269 private void clearCache() throws IOException { 270 for (File f : new File(cacheFolder).listFiles()) { 271 if (f.isDirectory()) 272 FileUtils.deleteDirectory(f); 273 else if (!f.getName().equals("packages.ini")) 274 FileUtils.forceDelete(f); 275 } 276 IniFile ini = new IniFile(Utilities.path(cacheFolder, "packages.ini")); 277 ini.removeSection("packages"); 278 ini.save(); 279 } 280 281// private void checkDeleteVersion(String id, String ver, int minVer) { 282// if (hasPackage(id, ver)) { 283// boolean del = true; 284// NpmPackage pck; 285// try { 286// pck = loadPackageFromCacheOnly(id, ver); 287// if (pck.getNpm().has("tools-version")) { 288// del = pck.getNpm().get("tools-version").getAsInt() < minVer; 289// } 290// } catch (Exception e) { 291// } 292// if (del) 293// try { 294// removePackage(id, ver); 295// } catch (IOException e) { 296// } 297// } 298// } 299 300//private void convertPackageCacheFrom1To2() throws IOException { 301//for (File f : new File(cacheFolder).listFiles()) { 302// if (f.isDirectory() && f.getName().contains("-")) { 303// String s = f.getName(); 304// int i = s.lastIndexOf("-"); 305// s = s.substring(0, i)+"#"+s.substring(i+1); 306// File nf = new File(Utilities.path(cacheFolder, s)); 307// if (!f.renameTo(nf)) 308// throw new IOException("Unable to rename "+f.getAbsolutePath()+" to "+nf.getAbsolutePath()); 309// } 310//} 311//} 312 313 // ========================= URL maps (while waiting for package registry) ============================================================================ 314 315 public boolean initUrlMaps(IniFile ini, boolean save) { 316 save = checkIniHasMapping("hl7.fhir.core", "http://hl7.org/fhir", ini) || save; 317 save = checkIniHasMapping("hl7.fhir.pubpack", "http://fhir.org/packages/hl7.fhir.pubpack", ini) || save; 318 save = checkIniHasMapping("hl7.fhir.xver-extensions", "http://fhir.org/packages/hl7.fhir.xver-extensions", ini) || save; 319 320 save = checkIniHasMapping("hl7.fhir.r2.core", "http://hl7.org/fhir/DSTU2/hl7.fhir.r2.core.tgz", ini) || save; 321 save = checkIniHasMapping("hl7.fhir.r2.examples", "http://hl7.org/fhir/DSTU2/hl7.fhir.r2.examples.tgz", ini) || save; 322 save = checkIniHasMapping("hl7.fhir.r2.elements", "http://hl7.org/fhir/DSTU2/hl7.fhir.r2.elements.tgz", ini) || save; 323 save = checkIniHasMapping("hl7.fhir.r2.expansions", "http://hl7.org/fhir/DSTU2/hl7.fhir.r2.expansions.tgz", ini) || save; 324 save = checkIniHasMapping("hl7.fhir.r2b.core", "http://hl7.org/fhir/2016May/hl7.fhir.r2b.core.tgz", ini) || save; 325 save = checkIniHasMapping("hl7.fhir.r2b.examples", "http://hl7.org/fhir/2016May/hl7.fhir.r2b.examples.tgz", ini) || save; 326 save = checkIniHasMapping("hl7.fhir.r2b.elements", "http://hl7.org/fhir/2016May/hl7.fhir.r2b.elements.tgz", ini) || save; 327 save = checkIniHasMapping("hl7.fhir.r2b.expansions", "http://hl7.org/fhir/2016May/hl7.fhir.r2b.expansions.tgz", ini) || save; 328 save = checkIniHasMapping("hl7.fhir.r3.core", "http://hl7.org/fhir/STU3/hl7.fhir.r3.core.tgz", ini) || save; 329 save = checkIniHasMapping("hl7.fhir.r3.examples", "http://hl7.org/fhir/STU3/hl7.fhir.r3.examples.tgz", ini) || save; 330 save = checkIniHasMapping("hl7.fhir.r3.elements", "http://hl7.org/fhir/STU3/hl7.fhir.r3.elements.tgz", ini) || save; 331 save = checkIniHasMapping("hl7.fhir.r3.expansions", "http://hl7.org/fhir/STU3/hl7.fhir.r3.expansions.tgz", ini) || save; 332 save = checkIniHasMapping("hl7.fhir.r4.core", "http://hl7.org/fhir/R4/hl7.fhir.r4.core.tgz", ini) || save; 333 save = checkIniHasMapping("hl7.fhir.r4.examples", "http://hl7.org/fhir/R4/hl7.fhir.r4.examples.tgz", ini) || save; 334 save = checkIniHasMapping("hl7.fhir.r4.elements", "http://hl7.org/fhir/R4/hl7.fhir.r4.elements.tgz", ini) || save; 335 save = checkIniHasMapping("hl7.fhir.r4.expansions", "http://hl7.org/fhir/R4/hl7.fhir.r4.expansions.tgz", ini) || save; 336 337 save = checkIniHasMapping("hl7.fhir.r5.core", "http://hl7.org/fhir/2020Feb/hl7.fhir.r5.core.tgz", ini) || save; 338 save = checkIniHasMapping("hl7.fhir.r5.expansions", "http://hl7.org/fhir/2020Feb/hl7.fhir.r5.expansions.tgz", ini) || save; 339 340 save = checkIniHasMapping("fhir.argonaut.ehr", "http://fhir.org/guides/argonaut", ini) || save; 341 save = checkIniHasMapping("fhir.argonaut.pd", "http://fhir.org/guides/argonaut-pd", ini) || save; 342 save = checkIniHasMapping("fhir.argonaut.scheduling", "http://fhir.org/guides/argonaut-scheduling", ini) || save; 343 save = checkIniHasMapping("fhir.hspc.acog", "http://hl7.org/fhir/fpar", ini) || save; 344 save = checkIniHasMapping("hl7.fhir.au.argonaut", "http://hl7.org.au/fhir/argonaut", ini) || save; 345 save = checkIniHasMapping("hl7.fhir.au.base", "http://hl7.org.au/fhir/base", ini) || save; 346 save = checkIniHasMapping("hl7.fhir.au.pd", "http://hl7.org.au/fhir/pd", ini) || save; 347 save = checkIniHasMapping("hl7.fhir.smart", "http://hl7.org/fhir/smart-app-launch", ini) || save; 348 save = checkIniHasMapping("hl7.fhir.snomed", "http://hl7.org/fhir/ig/snomed", ini) || save; 349 save = checkIniHasMapping("hl7.fhir.test.v10", "http://hl7.org/fhir/test-ig-10", ini) || save; 350 save = checkIniHasMapping("hl7.fhir.test.v30", "http://hl7.org/fhir/test", ini) || save; 351 save = checkIniHasMapping("hl7.fhir.us.breastcancer", "http://hl7.org/fhir/us/breastcancer", ini) || save; 352 save = checkIniHasMapping("hl7.fhir.us.ccda", "http://hl7.org/fhir/us/ccda", ini) || save; 353 save = checkIniHasMapping("hl7.fhir.us.cds.opioids", "http://hl7.org/fhir/ig/opioid-cds", ini) || save; 354 save = checkIniHasMapping("hl7.fhir.us.core", "http://hl7.org/fhir/us/core", ini) || save; 355 save = checkIniHasMapping("hl7.fhir.us.dafresearch", "http://hl7.org/fhir/us/daf-research", ini) || save; 356 save = checkIniHasMapping("hl7.fhir.us.ecr", "http://fhir.hl7.org/us/ecr", ini) || save; 357 save = checkIniHasMapping("hl7.fhir.us.hai", "http://hl7.org/fhir/us/hai", ini) || save; 358 save = checkIniHasMapping("hl7.fhir.us.hedis", "http://hl7.org/fhir/us/hedis", ini) || save; 359 save = checkIniHasMapping("hl7.fhir.us.meds", "http://hl7.org/fhir/us/meds", ini) || save; 360 save = checkIniHasMapping("hl7.fhir.us.qicore", "http://hl7.org/fhir/us/qicore", ini) || save; 361 save = checkIniHasMapping("hl7.fhir.us.sdc", "http://hl7.org/fhir/us/sdc", ini) || save; 362 save = checkIniHasMapping("hl7.fhir.us.sdcde", "http://hl7.org/fhir/us/sdcde", ini) || save; 363 save = checkIniHasMapping("hl7.fhir.uv.genomicsreporting", "http://hl7.org/fhir/uv/genomics-reporting", ini) || save; 364 save = checkIniHasMapping("hl7.fhir.uv.immds", "http://hl7.org/fhir/uv/cdsi", ini) || save; 365 save = checkIniHasMapping("hl7.fhir.uv.ips", "http://hl7.org/fhir/uv/ips", ini) || save; 366 save = checkIniHasMapping("hl7.fhir.uv.phd", "http://hl7.org/fhir/devices", ini) || save; 367 save = checkIniHasMapping("hl7.fhir.uv.vhdir", "http://hl7.org/fhir/ig/vhdir", ini) || save; 368 save = checkIniHasMapping("hl7.fhir.vn.base", "http://hl7.org/fhir/ig/vietnam", ini) || save; 369 save = checkIniHasMapping("hl7.fhir.uv.bulkdata", "http://hl7.org/fhir/uv/bulkdata", ini) || save; 370 save = checkIniHasMapping("hl7.fhir.us.bulkdata", "http://hl7.org/fhir/uv/bulkdata", ini) || save; 371 save = checkIniHasMapping("hl7.fhir.vocabpoc", "http://hl7.org/fhir/ig/vocab-poc", ini) || save; 372 return save; 373 } 374 375 private boolean checkIniHasMapping(String pid, String curl, IniFile ini) { 376 if (curl.equals(ini.getStringProperty("urls", pid))) 377 return false; 378 ini.setStringProperty("urls", pid, curl, null); 379 return true; 380 } 381 382 public void recordMap(String url, String id) throws IOException { 383 if (url == null) 384 return; 385 386 if (!(new File(Utilities.path(cacheFolder, "packages.ini")).exists())) 387 throw new Error("File "+Utilities.path(cacheFolder, "packages.ini")+" not found #1"); 388 IniFile ini = new IniFile(Utilities.path(cacheFolder, "packages.ini")); 389 ini.setStringProperty("urls", id, url, null); 390 if (!CACHE_VERSION.equals(ini.getStringProperty("cache", "version"))) { 391 throw new Error("File "+Utilities.path(cacheFolder, "packages.ini")+" cache version mismatch: expected '"+CACHE_VERSION+"', found '"+ini.getStringProperty("cache", "version")+"'"); 392 } 393 if (!(new File(Utilities.path(cacheFolder, "packages.ini")).exists())) 394 throw new Error("File "+Utilities.path(cacheFolder, "packages.ini")+" not found #2"); 395 396 ini.save(); 397 if (!(new File(Utilities.path(cacheFolder, "packages.ini")).exists())) 398 throw new Error("File "+Utilities.path(cacheFolder, "packages.ini")+" not found #3"); 399 } 400 401 public String getPackageUrl(String id) throws IOException { 402 IniFile ini = new IniFile(Utilities.path(cacheFolder, "packages.ini")); 403 return ini.getStringProperty("urls", id); 404 } 405 406 public String getPackageId(String url) throws IOException { 407 IniFile ini = new IniFile(Utilities.path(cacheFolder, "packages.ini")); 408 String[] ids = ini.getPropertyNames("urls"); 409 if (ids != null) { 410 for (String id : ids) { 411 if (url.equals(ini.getStringProperty("urls", id))) 412 return id; 413 } 414 } 415 return null; 416 } 417 418 public void loadFromBuildServer() throws IOException, ParseException { 419 buildLoaded = true; // whether it succeeds or not 420 URL url = new URL("https://build.fhir.org/ig/qas.json?nocache=" + System.currentTimeMillis()); 421 HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 422 connection.setRequestMethod("GET"); 423 InputStream json = connection.getInputStream(); 424 buildInfo = (JsonArray) new com.google.gson.JsonParser().parse(TextFile.streamToString(json)); 425 426 List<BuildRecord> builds = new ArrayList<>(); 427 428 for (JsonElement n : buildInfo) { 429 JsonObject o = (JsonObject) n; 430 if (o.has("url") && o.has("package-id") && o.get("package-id").getAsString().contains(".")) { 431 String u = o.get("url").getAsString(); 432 if (u.contains("/ImplementationGuide/")) 433 u = u.substring(0, u.indexOf("/ImplementationGuide/")); 434 builds.add(new BuildRecord(u, o.get("package-id").getAsString(), o.get("repo").getAsString(), readDate(o.get("date").getAsString()))); 435 } 436 } 437 Collections.sort(builds, new BuildRecordSorter()); 438 for (BuildRecord bld : builds) { 439 if (!ciList.containsKey(bld.getPackageId())) { 440 recordMap(bld.getUrl(), bld.getPackageId()); 441 ciList.put(bld.getPackageId(), "https://build.fhir.org/ig/"+bld.getRepo()); 442 } 443 } 444 } 445 446 private Date readDate(String s) throws ParseException { 447 SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM, yyyy HH:mm:ss Z", new Locale("en", "US")); 448 return sdf.parse(s); 449 } 450 451 public boolean isBuildLoaded() { 452 return buildLoaded; 453 } 454 455 456 public String buildPath(String url) { 457 for (JsonElement e : buildInfo) { 458 JsonObject j = (JsonObject) e; 459 if (j.has("url") && (url.equals(j.get("url").getAsString()) || j.get("url").getAsString().startsWith(url+"/ImplementationGuide"))) { 460 return "https://build.fhir.org/ig/"+j.get("repo").getAsString(); 461 } 462 } 463 return null; 464 } 465 466 public boolean checkBuildLoaded() throws IOException, ParseException { 467 if (isBuildLoaded()) 468 return true; 469 loadFromBuildServer(); 470 return false; 471 } 472 473 public Map<String, String> getCiList() { 474 return ciList; 475 } 476 477 public List<String> getUrls() throws IOException { 478 if (allUrls == null) 479 { 480 IniFile ini = new IniFile(Utilities.path(cacheFolder, "packages.ini")); 481 allUrls = new ArrayList<>(); 482 for (String s : ini.getPropertyNames("urls")) 483 allUrls.add(ini.getStringProperty("urls", s)); 484 try { 485 URL url = new URL("https://raw.githubusercontent.com/FHIR/ig-registry/master/fhir-ig-list.json?nocache=" + System.currentTimeMillis()); 486 HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 487 connection.setRequestMethod("GET"); 488 InputStream json = connection.getInputStream(); 489 JsonObject packages = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.streamToString(json)); 490 JsonArray guides = packages.getAsJsonArray("guides"); 491 for (JsonElement g : guides) { 492 JsonObject gi = (JsonObject) g; 493 if (gi.has("canonical")) 494 if (!allUrls.contains(gi.get("canonical").getAsString())) 495 allUrls.add(gi.get("canonical").getAsString()); 496 } 497 } catch (Exception e) { 498 System.out.println("Listing known Implementation Guides failed: "+e.getMessage()); 499 } 500 } 501 return allUrls; 502 } 503 504 // ========================= Package API ============================================================================ 505 506 public void removePackage(String id, String ver) throws IOException { 507 String f = Utilities.path(cacheFolder, id+"#"+ver); 508 Utilities.clearDirectory(f); 509 IniFile ini = new IniFile(Utilities.path(cacheFolder, "packages.ini")); 510 ini.removeProperty("packages", id+"#"+ver); 511 ini.save(); 512 new File(f).delete(); 513 } 514 515 /** 516 * get the latest version of the package from what is in the cache 517 * @param id 518 * @return 519 * @throws IOException 520 */ 521 public NpmPackage loadPackageFromCacheOnly(String id) throws IOException { 522 List<String> l = sorted(new File(cacheFolder).list()); 523 for (int i = l.size()-1; i >= 0; i--) { 524 String f = l.get(i); 525 if (f.startsWith(id+"#")) { 526 return loadPackageInfo(Utilities.path(cacheFolder, f)); 527 } 528 } 529 return null; 530 } 531 532 /** 533 * Load the identified package from the cache - it it exists 534 * 535 * This is for special purpose only. Generally, use the loadPackage method 536 * 537 * @param id 538 * @param version 539 * @return 540 * @throws IOException 541 */ 542 public NpmPackage loadPackageFromCacheOnly(String id, String version) throws IOException { 543 for (NpmPackage p : temporaryPackages) { 544 if (p.name().equals(id) && ("current".equals(version) || "dev".equals(version) || p.version().equals(version))) 545 return p; 546 } 547 for (String f : sorted(new File(cacheFolder).list())) { 548 if (f.equals(id+"#"+version)) { 549 return loadPackageInfo(Utilities.path(cacheFolder, f)); 550 } 551 } 552 if ("dev".equals(version)) 553 return loadPackageFromCacheOnly(id, "current"); 554 else 555 return null; 556 } 557 558 /** 559 * Add an already fetched package to the cache 560 */ 561 public NpmPackage addPackageToCache(String id, String version, InputStream tgz, String sourceDesc) throws IOException { 562 if (progress ) { 563 System.out.println("Installing "+id+"#"+(version == null ? "?" : version)+" to the package cache"); 564 System.out.print(" Fetching:"); 565 } 566 567 NpmPackage npm = NpmPackage.fromPackage(tgz, sourceDesc, true); 568 569 recordMap(npm.canonical(), npm.name()); 570 571 if (progress ) { 572 System.out.println(); 573 System.out.print(" Installing: "); 574 } 575 if (npm.name() == null || id == null || !id.equals(npm.name())) { 576 if (!id.equals("hl7.fhir.r5.core")) {// temporary work around 577 throw new IOException("Attempt to import a mis-identified package. Expected "+id+", got "+npm.name()); 578 } 579 } 580 if (version == null) 581 version = npm.version(); 582 583 String packRoot = Utilities.path(cacheFolder, id+"#"+version); 584 Utilities.createDirectory(packRoot); 585 Utilities.clearDirectory(packRoot); 586 587 int i = 0; 588 int c = 0; 589 int size = 0; 590 for (Entry<String, NpmPackageFolder> e : npm.getFolders().entrySet()) { 591 String dir = e.getKey().equals("package") ? Utilities.path(packRoot, "package") : Utilities.path(packRoot, "package", e.getKey());; 592 if (!(new File(dir).exists())) 593 Utilities.createDirectory(dir); 594 for (Entry<String, byte[]> fe : e.getValue().getContent().entrySet()) { 595 String fn = Utilities.path(dir, fe.getKey()); 596 byte[] cnt = fe.getValue(); 597 TextFile.bytesToFile(cnt, fn); 598 size = size + cnt.length; 599 i++; 600 if (progress && i % 50 == 0) { 601 c++; 602 System.out.print("."); 603 if (c == 120) { 604 System.out.println(""); 605 System.out.print(" "); 606 c = 2; 607 } 608 } 609 } 610 } 611 612 613 IniFile ini = new IniFile(Utilities.path(cacheFolder, "packages.ini")); 614 ini.setTimeStampFormat("yyyyMMddhhmmss"); 615 ini.setTimestampProperty("packages", id+"#"+version, Timestamp.from(Instant.now()), null); 616 ini.setIntegerProperty("package-sizes", id+"#"+version, size, null); 617 ini.save(); 618 if (progress) 619 System.out.println(" done."); 620 621 NpmPackage pck = loadPackageInfo(packRoot); 622 if (!id.equals(JSONUtil.str(npm.getNpm(), "name")) || !version.equals(JSONUtil.str(npm.getNpm(), "version"))) { 623 if (!id.equals(JSONUtil.str(npm.getNpm(), "name"))) { 624 npm.getNpm().addProperty("original-name", JSONUtil.str(npm.getNpm(), "name")); 625 npm.getNpm().remove("name"); 626 npm.getNpm().addProperty("name", id); 627 } 628 if (!version.equals(JSONUtil.str(npm.getNpm(), "version"))) { 629 npm.getNpm().addProperty("original-version", JSONUtil.str(npm.getNpm(), "version")); 630 npm.getNpm().remove("version"); 631 npm.getNpm().addProperty("version", version); 632 } 633 TextFile.stringToFile(new GsonBuilder().setPrettyPrinting().create().toJson(npm.getNpm()), Utilities.path(cacheFolder, id+"#"+version, "package", "package.json"), false); 634 } 635 636 return pck; 637 } 638 639 public NpmPackage loadPackage(String id) throws FHIRException, IOException { 640 throw new Error("Not done yet"); 641 } 642 643 public NpmPackage loadPackage(String id, String v) throws FHIRException, IOException { 644 NpmPackage p = loadPackageFromCacheOnly(id, v); 645 if (p != null) { 646 if ("current".equals(v)) { 647 p = checkCurrency(id, p); 648 } 649 if (p != null) 650 return p; 651 } 652 653 if ("dev".equals(v)) { 654 p = loadPackageFromCacheOnly(id, "current"); 655 p = checkCurrency(id, p); 656 if (p != null) 657 return p; 658 v = "current"; 659 } 660 661 String url = getPackageUrl(id); 662 if (url == null) 663 throw new FHIRException("Unable to resolve the package '"+id+"'"); 664 if (url.contains(".tgz")) { 665 InputStream stream = fetchFromUrlSpecific(url, true); 666 if (stream != null) 667 return addPackageToCache(id, v, stream, url); 668 throw new FHIRException("Unable to find the package source for '"+id+"' at "+url); 669 } 670 if (v == null) { 671 InputStream stream = fetchFromUrlSpecific(Utilities.pathURL(url, "package.tgz"), true); 672 if (stream == null && isBuildLoaded()) { 673 stream = fetchFromUrlSpecific(Utilities.pathURL(buildPath(url), "package.tgz"), true); 674 } 675 if (stream != null) 676 return addPackageToCache(id, null, stream, url); 677 throw new FHIRException("Unable to find the package source for '"+id+"' at "+url); 678 } else if ("current".equals(v) && ciList.containsKey(id)){ 679 InputStream stream = fetchFromUrlSpecific(Utilities.pathURL(ciList.get(id), "package.tgz"), true); 680 return addPackageToCache(id, v, stream, Utilities.pathURL(ciList.get(id), "package.tgz")); 681 } else { 682 String pu = Utilities.pathURL(url, "package-list.json"); 683 JsonObject json; 684 try { 685 json = fetchJson(pu); 686 } catch (Exception e) { 687 String pv = Utilities.pathURL(url, v, "package.tgz"); 688 try { 689 InputStream stream = fetchFromUrlSpecific(pv, true); 690 return addPackageToCache(id, v, stream, pv); 691 } catch (Exception e1) { 692 throw new FHIRException("Error fetching package directly ("+pv+"), or fetching package list for "+id+" from "+pu+": "+e1.getMessage(), e1); 693 } 694 } 695 if (!id.equals(JSONUtil.str(json, "package-id"))) 696 throw new FHIRException("Package ids do not match in "+pu+": "+id+" vs "+JSONUtil.str(json, "package-id")); 697 for (JsonElement e : json.getAsJsonArray("list")) { 698 JsonObject vo = (JsonObject) e; 699 if (v.equals(JSONUtil.str(vo, "version"))) { 700 InputStream stream = fetchFromUrlSpecific(Utilities.pathURL(JSONUtil.str(vo, "path"), "package.tgz"), true); 701 if (stream == null) 702 throw new FHIRException("Unable to find the package source for '"+id+"#"+v+"' at "+Utilities.pathURL(JSONUtil.str(vo, "path"), "package.tgz")); 703 return addPackageToCache(id, v, stream, Utilities.pathURL(JSONUtil.str(vo, "path"), "package.tgz")); 704 } 705 } 706// // special case: current version 707// if (id.equals("hl7.fhir.core") && v.equals(currentVersion)) { 708// InputStream stream = fetchFromUrlSpecific(Utilities.pathURL("http://build.fhir.org", "package.tgz"), true); 709// if (stream == null) 710// throw new FHIRException("Unable to find the package source for '"+id+"#"+v+"' at "+Utilities.pathURL("http://build.fhir.org", "package.tgz")); 711// return addPackageToCache(id, v, stream); 712// } 713 throw new FHIRException("Unable to resolve version "+v+" for package "+id); 714 } 715 } 716 717 718// public void loadFromFolder(String packagesFolder) throws IOException { 719// for (File f : new File(packagesFolder).listFiles()) { 720// if (f.getName().endsWith(".tgz")) { 721// temporaryPackages.add(extractLocally(new FileInputStream(f), f.getName())); 722// } 723// } 724// } 725// 726 727 /** 728 * turn true if a package exists 729 * @param id 730 * @param version 731 * @return 732 */ 733 public boolean hasPackage(String id, String version) { 734 for (NpmPackage p : temporaryPackages) { 735 if (p.name().equals(id) && ("current".equals(version) || "dev".equals(version) || p.version().equals(version))) 736 return true; 737 } 738 for (String f : sorted(new File(cacheFolder).list())) { 739 if (f.equals(id+"#"+version)) { 740 return true; 741 } 742 } 743 if ("dev".equals(version)) 744 return hasPackage(id, "current"); 745 else 746 return false; 747 } 748 749 750 /** 751 * List which versions of a package are available 752 * 753 * @param url 754 * @return 755 * @throws IOException 756 */ 757 758 public VersionHistory listVersions(String url) throws IOException { 759 if (historyCache.containsKey(url)) 760 return historyCache.get(url); 761 762 URL url1 = new URL(Utilities.pathURL(url, "package-list.json")+"?nocache=" + System.currentTimeMillis()); 763 HttpURLConnection connection = (HttpURLConnection) url1.openConnection(); 764 connection.setRequestMethod("GET"); 765 InputStream json = connection.getInputStream(); 766 JsonObject packageList = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.streamToString(json)); 767 VersionHistory res = new VersionHistory(); 768 res.id = JSONUtil.str(packageList, "package-id"); 769 res.canonical = JSONUtil.str(packageList, "canonical"); 770 for (JsonElement j : packageList.getAsJsonArray("list")) { 771 JsonObject jo = (JsonObject) j; 772 if ("current".equals(JSONUtil.str(jo, "version"))) 773 res.current = JSONUtil.str(jo, "path"); 774 else 775 res.versions.put(JSONUtil.str(jo, "version"), JSONUtil.str(jo, "path")); 776 } 777 historyCache.put(url, res); 778 return res; 779 } 780 781 782 /** 783 * Clear the cache 784 * 785 * @throws IOException 786 */ 787 public void clear() throws IOException { 788 clearCache(); 789 } 790 791 public void loadFromFolder(String packagesFolder) throws IOException { 792 for (File f : new File(packagesFolder).listFiles()) { 793 if (f.getName().endsWith(".tgz")) { 794 temporaryPackages.add(NpmPackage.fromPackage(new FileInputStream(f))); 795 } 796 } 797 } 798}