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}