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.IniFile; 026import org.hl7.fhir.utilities.TextFile; 027import org.hl7.fhir.utilities.Utilities; 028import org.hl7.fhir.utilities.VersionUtilities; 029import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; 030import org.hl7.fhir.utilities.http.HTTPResult; 031import org.hl7.fhir.utilities.http.ManagedWebAccess; 032 033public class TerminologyCacheManager { 034 035 // if either the CACHE_VERSION of the stated maj/min server versions change, the 036 // cache will be blown. Note that the stated terminology server version is 037 // the CapabilityStatement.software.version 038 private static final String CACHE_VERSION = "1"; 039 040 private String cacheFolder; 041 private String version; 042 private String ghOrg; 043 private String ghRepo; 044 private String ghBranch; 045 046 public TerminologyCacheManager(String serverVersion, String rootDir, String ghOrg, String ghRepo, String ghBranch) throws IOException { 047 super(); 048 // this.rootDir = rootDir; 049 this.ghOrg = ghOrg; 050 this.ghRepo = ghRepo; 051 this.ghBranch = ghBranch; 052 053 version = CACHE_VERSION+"/"+VersionUtilities.getMajMin(serverVersion); 054 055 if (Utilities.noString(ghOrg) || Utilities.noString(ghRepo) || Utilities.noString(ghBranch)) { 056 cacheFolder = Utilities.path(rootDir, "temp", "tx-cache"); 057 } else { 058 cacheFolder = Utilities.path(System.getProperty("user.home"), ".fhir", "tx-cache", ghOrg, ghRepo, ghBranch); 059 } 060 } 061 062 public void initialize() throws IOException { 063 File f = ManagedFileAccess.file(cacheFolder); 064 if (!f.exists()) { 065 Utilities.createDirectory(cacheFolder); 066 } 067 if (!version.equals(getCacheVersion())) { 068 clearCache(); 069 fillCache("https://tx.fhir.org/tx-cache/"+ghOrg+"/"+ghRepo+"/"+ghBranch+".zip"); 070 } 071 if (!version.equals(getCacheVersion())) { 072 clearCache(); 073 fillCache("https://tx.fhir.org/tx-cache/"+ghOrg+"/"+ghRepo+"/default.zip"); 074 } 075 if (!version.equals(getCacheVersion())) { 076 clearCache(); 077 } 078 079 IniFile ini = new IniFile(Utilities.path(cacheFolder, "cache.ini")); 080 ini.setStringProperty("cache", "version", version, null); 081 ini.setDateProperty("cache", "last-use", new Date(), null); 082 ini.save(); 083 } 084 085 private void fillCache(String source) throws IOException { 086 try { 087 System.out.println("Initialise terminology cache from "+source); 088 089 HTTPResult res = ManagedWebAccess.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 = ManagedFileAccess.fromPath(path).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 HTTPResult res = ManagedWebAccess.builder() 160 .withBasicAuth(token.substring(0, token.indexOf(':')), token.substring(token.indexOf(':') + 1)) 161 .withAccept("application/zip").put(url, bs.toByteArray(), null); 162 163 if (res.getCode() >= 300) { 164 System.out.println("sending cache failed: "+res.getCode()); 165 } else { 166 System.out.println("Sent cache"); 167 } 168 } 169 170}