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