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}