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}