001package org.hl7.fhir.utilities.cache;
002
003import java.io.BufferedOutputStream;
004import java.io.ByteArrayInputStream;
005import java.io.ByteArrayOutputStream;
006import java.io.File;
007import java.io.FileInputStream;
008import java.io.IOException;
009import java.io.InputStream;
010import java.util.ArrayList;
011import java.util.Collections;
012import java.util.HashMap;
013import java.util.List;
014import java.util.Map;
015import java.util.Map.Entry;
016import java.util.zip.ZipEntry;
017import java.util.zip.ZipInputStream;
018
019import org.apache.commons.io.FileUtils;
020import org.hl7.fhir.utilities.IniFile;
021import org.hl7.fhir.utilities.TextFile;
022import org.hl7.fhir.utilities.Utilities;
023import org.hl7.fhir.utilities.cache.PackageGenerator.PackageType;
024
025import com.google.gson.JsonObject;
026
027/**
028   * info and loader for a package 
029   * 
030   * Packages may exist on disk in the cache, or purely in memory when they are loaded on the fly
031   * 
032   * Packages are contained in subfolders (see the package spec). The FHIR resources will be in "package"
033   * 
034   * @author Grahame Grieve
035   *
036   */
037  public class NpmPackage {
038
039    private String path;
040    private List<String> folders = new ArrayList<String>();
041    private Map<String, byte[]> content = new HashMap<String, byte[]>();
042    private JsonObject npm;
043    private IniFile cache;
044
045    public NpmPackage(JsonObject npm, Map<String, byte[]> content, List<String> folders) {
046      this.path = null;
047      this.content = content;
048      this.npm = npm;
049      this.folders = folders;
050    }
051    
052    public NpmPackage(String path) throws IOException {
053      this.path = path;
054      if (path != null) {
055        for (String f : sorted(new File(path).list())) {
056          if (new File(Utilities.path(path, f)).isDirectory()) {
057            folders.add(f); 
058          }
059        }
060        npm = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.fileToString(Utilities.path(path, "package", "package.json")));
061        cache = new IniFile(Utilities.path(path, "cache.ini"));        
062      }
063    }
064    
065    private List<String> sorted(String[] keys) {
066      List<String> names = new ArrayList<String>();
067      for (String s : keys)
068        names.add(s);
069      Collections.sort(names);
070      return names;
071    }
072
073    public static NpmPackage fromZip(InputStream stream, boolean dropRootFolder) throws IOException {
074      NpmPackage res = new NpmPackage(null);
075      ZipInputStream zip = new ZipInputStream(stream);
076      ZipEntry ze;
077      while ((ze = zip.getNextEntry()) != null) {
078        int size;
079        byte[] buffer = new byte[2048];
080
081        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
082        BufferedOutputStream bos = new BufferedOutputStream(bytes, buffer.length);
083
084        while ((size = zip.read(buffer, 0, buffer.length)) != -1) {
085          bos.write(buffer, 0, size);
086        }
087        bos.flush();
088        bos.close();
089        if (bytes.size() > 0)
090          if (dropRootFolder)
091            res.content.put(ze.getName().substring(ze.getName().indexOf("/")+1), bytes.toByteArray());
092          else
093            res.content.put(ze.getName(), bytes.toByteArray());
094
095        zip.closeEntry();
096      }
097      zip.close();         
098      res.npm = (JsonObject) new com.google.gson.JsonParser().parse(new String(res.content.get("package/package.json")));
099      return res;
100    }
101   
102    
103    /**
104     * Accessing the contents of the package - get a list of files in a subfolder of the package 
105     *
106     * @param folder
107     * @return
108     * @throws IOException 
109     */
110    public List<String> list(String folder) throws IOException {
111      List<String> res = new ArrayList<String>();
112      if (path != null) {
113        File f = new File(Utilities.path(path, folder));
114        if (f.exists() && f.isDirectory())
115          for (String s : f.list())
116            res.add(s);
117      } else {
118        for (String s : content.keySet()) {
119          if (s.startsWith(folder+"/") && !s.substring(folder.length()+2).contains("/"))
120            res.add(s.substring(folder.length()+1));
121        }
122      }
123      return res;
124    }
125
126    /** 
127     * Copies all the files in the package folder [folder] to the nominated dest, 
128     * and returns a list of all the file names copied
129     *  
130     * @param folder
131     * @return
132     * @throws IOException 
133     */
134    public List<String> copyTo(String folder, String dest) throws IOException {
135      List<String> res = new ArrayList<String>();
136      if (path != null) {
137        copyToDest(Utilities.path(path, folder), Utilities.path(path, folder), dest, res);
138      } else {
139        for (Entry<String, byte[]> e : content.entrySet()) {
140          if (e.getKey().startsWith(folder+"/")) {
141            String s = e.getKey().substring(folder.length()+1);
142            res.add(s);
143            String dst = Utilities.path(dest, s);
144            String dstDir = Utilities.getDirectoryForFile(dst);
145            Utilities.createDirectory(dstDir);
146            TextFile.bytesToFile(e.getValue(), dst);
147          }
148        }
149      }
150      return res;
151    }
152    
153    private void copyToDest(String base, String folder, String dest, List<String> res) throws IOException {
154      for (File f : new File(folder).listFiles()) {
155        if (f.isDirectory()) {
156          copyToDest(base, f.getAbsolutePath(),  Utilities.path(dest, f.getName()), res);
157        } else {
158          String dst = Utilities.path(dest, f.getName());
159          FileUtils.copyFile(f, new File(dst), true);
160          res.add(f.getAbsolutePath().substring(base.length()+1));
161        }
162      }
163    }
164
165    /**
166     * get a stream that contains the contents of one of the files in a folder
167     * 
168     * @param folder
169     * @param file
170     * @return
171     * @throws IOException
172     */
173    public InputStream load(String folder, String file) throws IOException {
174      if (content.containsKey(folder+"/"+file))
175        return new ByteArrayInputStream(content.get(folder+"/"+file));
176      else {
177        File f = new File(Utilities.path(path, folder, file));
178        if (f.exists())
179          return new FileInputStream(f);
180        throw new IOException("Unable to find the file "+folder+"/"+file+" in the package "+name());
181      }
182    }
183
184    /**
185     * Handle to the package json file
186     * 
187     * @return
188     */
189    public JsonObject getNpm() {
190      return npm;
191    }
192    
193    /**
194     * convenience method for getting the package name
195     * @return
196     */
197    public String name() {
198      return npm.get("name").getAsString();
199    }
200
201    public String canonical() {
202      return npm.get("canonical").getAsString();
203    }
204
205    /**
206     * convenience method for getting the package version
207     * @return
208     */
209    public String version() {
210      return npm.get("version").getAsString();
211    }
212
213    /**
214     * convenience method for getting the package fhir version
215     * @return
216     */
217    public String fhirVersion() {
218      if ("hl7.fhir.core".equals(npm.get("name").getAsString()))
219        return npm.get("version").getAsString();
220      else
221        return npm.getAsJsonObject("dependencies").get("hl7.fhir.core").getAsString();
222    }
223
224    public String description() {
225      if (path != null)
226        return path;
227      else
228        return "memory";
229    }
230
231    public boolean isType(PackageType template) {
232      return template.getCode().equals(type());
233    }
234
235    public String type() {
236      return npm.get("type").getAsString();
237    }
238
239    public String getPath() {
240      return path;
241    }
242
243    public IniFile getCache() {
244      return cache;
245    }
246
247    /**
248     * only for use by the package manager itself
249     * 
250     * @param path
251     */
252    public void setPath(String path) {
253      this.path = path;
254    }
255
256    public String getWebLocation() {
257      if (npm.has("url"))
258        return npm.get("url").getAsString();
259      else
260        return npm.get("canonical").getAsString();
261    }
262
263
264  }