
001package org.hl7.fhir.r5.terminologies; 002 003import java.io.ByteArrayInputStream; 004import java.io.ByteArrayOutputStream; 005import java.io.File; 006import java.io.IOException; 007import java.io.InputStream; 008import java.io.OutputStream; 009import java.nio.file.Files; 010import java.nio.file.Path; 011import java.nio.file.Paths; 012import java.util.Arrays; 013import java.util.Date; 014import java.util.zip.ZipEntry; 015import java.util.zip.ZipInputStream; 016import java.util.zip.ZipOutputStream; 017 018import org.hl7.fhir.utilities.IniFile; 019import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage; 020import org.hl7.fhir.utilities.FileUtilities; 021import org.hl7.fhir.utilities.Utilities; 022import org.hl7.fhir.utilities.VersionUtilities; 023import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; 024import org.hl7.fhir.utilities.http.HTTPResult; 025import org.hl7.fhir.utilities.http.ManagedWebAccess; 026 027@MarkedToMoveToAdjunctPackage 028public class TerminologyCacheManager { 029 030 // if either the CACHE_VERSION of the stated maj/min server versions change, the 031 // cache will be blown. Note that the stated terminology server version is 032 // the CapabilityStatement.software.version 033 private static final String CACHE_VERSION = "1"; 034 035 private String cacheFolder; 036 private String version; 037 private String ghOrg; 038 private String ghRepo; 039 private String ghBranch; 040 041 public TerminologyCacheManager(String serverVersion, String rootDir, String ghOrg, String ghRepo, String ghBranch) throws IOException { 042 super(); 043 // this.rootDir = rootDir; 044 this.ghOrg = ghOrg; 045 this.ghRepo = ghRepo; 046 this.ghBranch = ghBranch; 047 048 version = CACHE_VERSION+"/"+VersionUtilities.getMajMin(serverVersion); 049 050 if (Utilities.noString(ghOrg) || Utilities.noString(ghRepo) || Utilities.noString(ghBranch)) { 051 cacheFolder = Utilities.path(rootDir, "temp", "tx-cache"); 052 } else { 053 cacheFolder = Utilities.path(System.getProperty("user.home"), ".fhir", "tx-cache", ghOrg, ghRepo, ghBranch); 054 } 055 } 056 057 public void initialize() throws IOException { 058 File f = ManagedFileAccess.file(cacheFolder); 059 if (!f.exists()) { 060 FileUtilities.createDirectory(cacheFolder); 061 } 062 if (!version.equals(getCacheVersion())) { 063 clearCache(); 064 fillCache("https://tx.fhir.org/tx-cache/"+ghOrg+"/"+ghRepo+"/"+ghBranch+".zip"); 065 } 066 if (!version.equals(getCacheVersion())) { 067 clearCache(); 068 fillCache("https://tx.fhir.org/tx-cache/"+ghOrg+"/"+ghRepo+"/default.zip"); 069 } 070 if (!version.equals(getCacheVersion())) { 071 clearCache(); 072 } 073 074 IniFile ini = new IniFile(Utilities.path(cacheFolder, "cache.ini")); 075 ini.setStringProperty("cache", "version", version, null); 076 ini.setDateProperty("cache", "last-use", new Date(), null); 077 ini.save(); 078 } 079 080 private void fillCache(String source) throws IOException { 081 try { 082 System.out.println("Initialise terminology cache from "+source); 083 084 HTTPResult res = ManagedWebAccess.get(Arrays.asList("web"), source+"?nocache=" + System.currentTimeMillis()); 085 res.checkThrowException(); 086 unzip(new ByteArrayInputStream(res.getContent()), cacheFolder); 087 } catch (Exception e) { 088 System.out.println("No - can't initialise cache from "+source+": "+e.getMessage()); 089 } 090 } 091 092 public static void unzip(InputStream is, String targetDir) throws IOException { 093 try (ZipInputStream zipIn = new ZipInputStream(is)) { 094 for (ZipEntry ze; (ze = zipIn.getNextEntry()) != null; ) { 095 Path path = Path.of(Utilities.path(targetDir, ze.getName())).normalize(); 096 String pathString = ManagedFileAccess.fromPath(path).getAbsolutePath(); 097 if (!path.startsWith(Path.of(targetDir).normalize())) { 098 // see: https://snyk.io/research/zip-slip-vulnerability 099 throw new RuntimeException("Entry with an illegal path: " + ze.getName()); 100 } 101 if (ze.isDirectory()) { 102 FileUtilities.createDirectory(pathString); 103 } else { 104 FileUtilities.createDirectory(FileUtilities.getDirectoryForFile(pathString)); 105 FileUtilities.streamToFileNoClose(zipIn, pathString); 106 } 107 } 108 } 109 } 110 111 private void clearCache() throws IOException { 112 FileUtilities.clearDirectory(cacheFolder); 113 } 114 115 private String getCacheVersion() throws IOException { 116 IniFile ini = new IniFile(Utilities.path(cacheFolder, "cache.ini")); 117 return ini.getStringProperty("cache", "version"); 118 } 119 120 public String getFolder() { 121 return cacheFolder; 122 } 123 124 private void zipDirectory(OutputStream outputStream) throws IOException { 125 try (ZipOutputStream zs = new ZipOutputStream(outputStream)) { 126 Path pp = Paths.get(cacheFolder); 127 Files.walk(pp) 128 .forEach(path -> { 129 try { 130 if (Files.isDirectory(path)) { 131 zs.putNextEntry(new ZipEntry(pp.relativize(path).toString() + "/")); 132 } else { 133 ZipEntry zipEntry = new ZipEntry(pp.relativize(path).toString()); 134 zs.putNextEntry(zipEntry); 135 Files.copy(path, zs); 136 zs.closeEntry(); 137 } 138 } catch (IOException e) { 139 System.err.println(e); 140 } 141 }); 142 } 143 } 144 145 146 public void commit(String token) throws IOException { 147 // create a zip of all the files 148 ByteArrayOutputStream bs = new ByteArrayOutputStream(); 149 zipDirectory(bs); 150 151 // post it to 152 String url = "https://tx.fhir.org/post/tx-cache/"+ghOrg+"/"+ghRepo+"/"+ghBranch+".zip"; 153 System.out.println("Sending tx-cache to "+url+" ("+Utilities.describeSize(bs.toByteArray().length)+")"); 154 HTTPResult res = ManagedWebAccess.accessor(Arrays.asList("web")) 155 .withBasicAuth(token.substring(0, token.indexOf(':')), token.substring(token.indexOf(':') + 1)) 156 .put(url, bs.toByteArray(), null, "application/zip"); 157 158 if (res.getCode() >= 300) { 159 System.out.println("sending cache failed: "+res.getCode()); 160 } else { 161 System.out.println("Sent cache"); 162 } 163 } 164 165}