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