001package org.hl7.fhir.utilities.cache;
002
003/*-
004 * #%L
005 * org.hl7.fhir.utilities
006 * %%
007 * Copyright (C) 2014 - 2019 Health Level 7
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 * 
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 * 
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023
024import java.io.BufferedOutputStream;
025import java.io.ByteArrayInputStream;
026import java.io.ByteArrayOutputStream;
027import java.io.File;
028import java.io.FileInputStream;
029import java.io.FileNotFoundException;
030import java.io.IOException;
031import java.io.InputStream;
032import java.io.OutputStream;
033import java.nio.charset.Charset;
034import java.util.ArrayList;
035import java.util.Collections;
036import java.util.HashMap;
037import java.util.List;
038import java.util.Map;
039import java.util.Map.Entry;
040import java.util.Set;
041import java.util.zip.ZipEntry;
042import java.util.zip.ZipInputStream;
043
044import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
045import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
046import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
047import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
048import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
049import org.apache.commons.io.FileUtils;
050import org.apache.commons.io.IOUtils;
051import org.hl7.fhir.exceptions.FHIRException;
052import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
053import org.hl7.fhir.utilities.IniFile;
054import org.hl7.fhir.utilities.TextFile;
055import org.hl7.fhir.utilities.Utilities;
056import org.hl7.fhir.utilities.cache.PackageCacheManager.PackageEntry;
057import org.hl7.fhir.utilities.cache.PackageGenerator.PackageType;
058import org.hl7.fhir.utilities.json.JSONUtil;
059import org.hl7.fhir.utilities.json.JsonTrackingParser;
060
061import com.google.common.base.Charsets;
062import com.google.gson.GsonBuilder;
063import com.google.gson.JsonArray;
064import com.google.gson.JsonElement;
065import com.google.gson.JsonObject;
066
067/**
068 * info and loader for a package 
069 * 
070 * Packages may exist on disk in the cache, or purely in memory when they are loaded on the fly
071 * 
072 * Packages are contained in subfolders (see the package spec). The FHIR resources will be in "package"
073 * 
074 * @author Grahame Grieve
075 *
076 */
077public class NpmPackage {
078
079  public static boolean isValidName(String pid) {
080    return pid.matches("^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9]*)+$");
081  }
082
083  public static boolean isValidVersion(String ver) {
084    return ver.matches("^[0-9]+\\.[0-9]+\\.[0-9]+$");
085  }
086
087  public class NpmPackageFolder {
088    private String name;
089    private Map<String, List<String>> types = new HashMap<>();
090    private Map<String, byte[]> content = new HashMap<String, byte[]>(); 
091    private JsonObject index;
092    private File folder;
093
094    public NpmPackageFolder(String name) {
095      super();
096      this.name = name;
097    }
098
099    public String getName() {
100      return name;
101    }
102
103    public void readIndex(JsonObject index) {
104      this.index = index;
105      for (JsonElement e : index.getAsJsonArray("files")) {
106        JsonObject file = (JsonObject) e;
107        String type = JSONUtil.str(file, "resourceType");
108        String name = JSONUtil.str(file, "filename");
109        if (!types.containsKey(type))
110          types.put(type, new ArrayList<>());
111        types.get(type).add(name);
112      }
113    }
114
115    public List<String> listFiles() {
116      List<String> res = new ArrayList<>();
117      if (folder != null) {
118        for (File f : folder.listFiles()) {
119          if (!f.isDirectory() && !Utilities.existsInList(f.getName(), "package.json", ".index.json")) {
120            res.add(f.getName());
121          }
122        }
123      } else {
124        for (String s : content.keySet()) {
125          if (!Utilities.existsInList(s, "package.json", ".index.json")) {
126            res.add(s);
127          }
128        }
129      }
130      Collections.sort(res);
131      return res;
132    }
133
134    public Map<String, byte[]> getContent() {
135      return content;
136    }
137
138    public byte[] fetchFile(String file) throws FileNotFoundException, IOException {
139      if (folder != null) {
140        File f = new File(Utilities.path(folder.getAbsolutePath(), file));
141        if (f.exists()) {
142          return TextFile.fileToBytes(f);
143        } else {
144          return null;
145        }
146      } else {
147        return content.get(file);
148      }
149    }
150
151    public boolean hasFile(String file) throws IOException {
152      if (folder != null) {
153        return new File(Utilities.path(folder.getAbsolutePath(), file)).exists();
154      } else {
155        return content.containsKey(file);
156      }
157
158    }
159
160    public String dump() {
161      return name + " ("+ (folder == null ? "null" : folder.toString())+") | "+Boolean.toString(index != null)+" | "+content.size()+" | "+types.size();
162    }
163
164  }
165
166  private String path;
167  private JsonObject npm;
168  private Map<String, NpmPackageFolder> folders = new HashMap<>();
169
170  private NpmPackage() {
171
172  }
173  //  public NpmPackage(JsonObject npm, Map<String, byte[]> content, List<String> folders) {
174  //    this.path = null;
175  //    this.content = content;
176  //    this.npm = npm;
177  //    this.folders = folders;
178  //  }
179
180  public static NpmPackage fromFolder(String path) throws IOException {
181    NpmPackage res = new NpmPackage();
182    loadFiles(res, path, new File(path));
183    res.checkIndexed(path);
184    return res;
185  }
186
187  public static void loadFiles(NpmPackage res, String path, File source, String... exemptions) throws FileNotFoundException, IOException {
188    res.npm = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.fileToString(Utilities.path(path, "package", "package.json")));
189
190    File dir = new File(path);
191    for (File f : dir.listFiles()) {
192      if (!Utilities.existsInList(f.getName(), ".git", ".svn") && !Utilities.existsInList(f.getName(), exemptions)) {
193        if (f.isDirectory()) {
194          String d = f.getName();
195          if (!d.equals("package")) {
196            d = Utilities.path("package", d);
197          }
198          NpmPackageFolder folder = res.new NpmPackageFolder(d);
199          folder.folder = f;
200          res.folders.put(d, folder);
201          File ij = new File(Utilities.path(f.getAbsolutePath(), ".index.json"));
202          if (ij.exists()) {
203            try {
204              folder.readIndex(JsonTrackingParser.parseJson(ij));
205            } catch (Exception e) {
206              throw new IOException("Error parsing "+ij.getAbsolutePath()+": "+e.getMessage(), e);
207            }
208          }
209          loadSubFolders(res, dir.getAbsolutePath(), f);
210        } else {
211          NpmPackageFolder folder = res.new NpmPackageFolder("package/$root");
212          folder.folder = dir;
213          res.folders.put("package/$root", folder);        
214        }
215      }
216    }
217  }
218
219  private static void loadSubFolders(NpmPackage res, String rootPath, File dir) throws IOException {
220    for (File f : dir.listFiles()) {
221      if (f.isDirectory()) {
222        String d = f.getAbsolutePath().substring(rootPath.length()+1);
223        if (!d.startsWith("package")) {
224          d = Utilities.path("package", d);
225        }
226        NpmPackageFolder folder = res.new NpmPackageFolder(d);
227        folder.folder = f;
228        res.folders.put(d, folder);
229        File ij = new File(Utilities.path(f.getAbsolutePath(), ".index.json"));
230        if (ij.exists()) {
231          try {
232            folder.readIndex(JsonTrackingParser.parseJson(ij));
233          } catch (Exception e) {
234            throw new IOException("Error parsing "+ij.getAbsolutePath()+": "+e.getMessage(), e);
235          }
236        }
237        loadSubFolders(res, rootPath, f);
238        
239      }
240    }    
241  }
242
243  public static NpmPackage fromFolder(String folder, PackageType defType, String... exemptions) throws IOException {
244    NpmPackage res = new NpmPackage();
245    loadFiles(res, folder, new File(folder), exemptions);
246    if (!res.folders.containsKey("package")) {
247      res.folders.put("package", res.new NpmPackageFolder("package"));
248    }
249    if (!res.folders.get("package").hasFile("package.json") && defType != null) {
250      TextFile.stringToFile("{ \"type\" : \""+defType.getCode()+"\"}", Utilities.path(res.folders.get("package").folder.getAbsolutePath(), "package.json"));
251    }
252    res.npm = (JsonObject) new com.google.gson.JsonParser().parse(new String(res.folders.get("package").fetchFile("package.json")));
253    return res;
254  }
255
256  private static final int BUFFER_SIZE = 1024;
257
258  public static NpmPackage fromPackage(InputStream tgz) throws IOException {
259    return fromPackage(tgz, null, false);
260  }
261
262  public static NpmPackage fromPackage(InputStream tgz, String desc) throws IOException {
263    return fromPackage(tgz, desc, false);
264  }
265
266  public static NpmPackage fromPackage(InputStream tgz, String desc, boolean progress) throws IOException {
267    NpmPackage res = new NpmPackage();
268    res.readStream(tgz, desc, progress);
269    return res;
270  }
271
272  public void readStream(InputStream tgz, String desc, boolean progress) throws IOException {
273    GzipCompressorInputStream gzipIn = new GzipCompressorInputStream(tgz);
274    try (TarArchiveInputStream tarIn = new TarArchiveInputStream(gzipIn)) {
275      TarArchiveEntry entry;
276
277      int i = 0;
278      int c = 12;
279      while ((entry = (TarArchiveEntry) tarIn.getNextEntry()) != null) {
280        i++;
281        String n = entry.getName();
282        if (entry.isDirectory()) {
283          String dir = n.substring(0, n.length()-1);
284          if (dir.startsWith("package/")) {
285            dir = dir.substring(8);
286          }
287          folders.put(dir, new NpmPackageFolder(dir));
288        } else {
289          int count;
290          byte data[] = new byte[BUFFER_SIZE];
291          ByteArrayOutputStream fos = new ByteArrayOutputStream();
292          try (BufferedOutputStream dest = new BufferedOutputStream(fos, BUFFER_SIZE)) {
293            while ((count = tarIn.read(data, 0, BUFFER_SIZE)) != -1) {
294              dest.write(data, 0, count);
295            }
296          }
297          fos.close();
298          loadFile(n, fos.toByteArray());
299        }
300        if (progress && i % 50 == 0) {
301          c++;
302          System.out.print(".");
303          if (c == 120) {
304            System.out.println("");
305            System.out.print("  ");
306            c = 2;
307          }
308        }
309      }
310    } 
311    try {
312      npm = JsonTrackingParser.parseJson(folders.get("package").fetchFile("package.json"));
313    } catch (Exception e) {
314      throw new IOException("Error parsing "+(desc == null ? "" : desc+"#")+"package/package.json: "+e.getMessage(), e);
315    }
316    checkIndexed(desc);
317  }
318
319  public void loadFile(String n, byte[] data) throws IOException {
320    String dir = n.contains("/") ? n.substring(0, n.lastIndexOf("/")) : "$root";
321    if (dir.startsWith("package/")) {
322      dir = dir.substring(8);
323    }
324    n = n.substring(n.lastIndexOf("/")+1);
325    NpmPackageFolder index = folders.get(dir);
326    if (index == null) {
327      index = new NpmPackageFolder(dir);
328      folders.put(dir, index);
329    }
330    index.content.put(n, data);
331  }
332
333  private void checkIndexed(String desc) throws IOException {
334    for (NpmPackageFolder folder : folders.values()) {
335      if (folder.index == null) {
336        NpmPackageIndexBuilder indexer = new NpmPackageIndexBuilder();
337        indexer.start();
338        for (String n : folder.listFiles()) {
339          indexer.seeFile(n, folder.fetchFile(n));
340        }       
341        String json = indexer.build();
342        try {
343          folder.readIndex(JsonTrackingParser.parseJson(json));
344        } catch (Exception e) {
345          TextFile.stringToFile(json, Utilities.path("[tmp]", ".index.json"));
346          throw new IOException("Error parsing "+(desc == null ? "" : desc+"#")+"package/"+folder.name+"/.index.json: "+e.getMessage(), e);
347        }
348      }
349    }
350  }
351
352
353  public static NpmPackage fromZip(InputStream stream, boolean dropRootFolder, String desc) throws IOException {
354    NpmPackage res = new NpmPackage();
355    ZipInputStream zip = new ZipInputStream(stream);
356    ZipEntry ze;
357    while ((ze = zip.getNextEntry()) != null) {
358      int size;
359      byte[] buffer = new byte[2048];
360
361      ByteArrayOutputStream bytes = new ByteArrayOutputStream();
362      BufferedOutputStream bos = new BufferedOutputStream(bytes, buffer.length);
363
364      while ((size = zip.read(buffer, 0, buffer.length)) != -1) {
365        bos.write(buffer, 0, size);
366      }
367      bos.flush();
368      bos.close();
369      if (bytes.size() > 0) {
370        if (dropRootFolder) {
371          res.loadFile(ze.getName().substring(ze.getName().indexOf("/")+1), bytes.toByteArray());
372        } else {
373          res.loadFile(ze.getName(), bytes.toByteArray());
374        }
375      }
376      zip.closeEntry();
377    }
378    zip.close();         
379    try {
380      res.npm = JsonTrackingParser.parseJson(res.folders.get("package").fetchFile("package.json"));
381    } catch (Exception e) {
382      throw new IOException("Error parsing "+(desc == null ? "" : desc+"#")+"package/package.json: "+e.getMessage(), e);
383    }
384    res.checkIndexed(desc);
385    return res;
386  }
387
388
389  /**
390   * Accessing the contents of the package - get a list of files in a subfolder of the package 
391   *
392   * @param folder
393   * @return
394   * @throws IOException 
395   */
396  public List<String> list(String folder) throws IOException {
397    List<String> res = new ArrayList<String>();
398    if (folders.containsKey(folder)) {
399      res.addAll(folders.get(folder).listFiles());
400    } else if (folders.containsKey(Utilities.path("package", folder))) {
401      res.addAll(folders.get(Utilities.path("package", folder)).listFiles());
402    }
403    return res;
404  }
405
406  public List<String> listResources(String... types) throws IOException {
407    List<String> res = new ArrayList<String>();
408    NpmPackageFolder folder = folders.get("package");
409    for (String s : types) {
410      if (folder.types.containsKey(s))
411        res.addAll(folder.types.get(s));
412    }
413    Collections.sort(res);
414    return res;
415  }
416
417  /**
418   * use the name from listResources()
419   * 
420   * @param id
421   * @return
422   * @throws IOException
423   */
424  public InputStream loadResource(String file) throws IOException {
425    NpmPackageFolder folder = folders.get("package");
426    return new ByteArrayInputStream(folder.fetchFile(file));
427  }
428
429  /**
430   * get a stream that contains the contents of one of the files in a folder
431   * 
432   * @param folder
433   * @param file
434   * @return
435   * @throws IOException
436   */
437  public InputStream load(String folder, String file) throws IOException {
438    NpmPackageFolder f = folders.get(folder);
439    if (f == null) {
440      f = folders.get(Utilities.path("package", folder));
441    }
442    if (f != null && f.hasFile(file)) {
443      return new ByteArrayInputStream(f.fetchFile(file));
444    } else {
445      throw new IOException("Unable to find the file "+folder+"/"+file+" in the package "+name());
446    }
447  }
448
449  public boolean hasFile(String folder, String file) throws IOException {
450    NpmPackageFolder f = folders.get(folder);
451    return f != null && f.hasFile(file);
452  }
453
454
455  /**
456   * Handle to the package json file
457   * 
458   * @return
459   */
460  public JsonObject getNpm() {
461    return npm;
462  }
463
464  /**
465   * convenience method for getting the package name
466   * @return
467   */
468  public String name() {
469    return JSONUtil.str(npm, "name");
470  }
471
472  public String date() {
473    return JSONUtil.str(npm, "date");
474  }
475
476  public String canonical() {
477    return JSONUtil.str(npm, "canonical");
478  }
479
480  /**
481   * convenience method for getting the package version
482   * @return
483   */
484  public String version() {
485    return JSONUtil.str(npm, "version");
486  }
487
488  /**
489   * convenience method for getting the package fhir version
490   * @return
491   */
492  public String fhirVersion() {
493    if ("hl7.fhir.core".equals(JSONUtil.str(npm, "name")))
494      return JSONUtil.str(npm, "version");
495    else if (JSONUtil.str(npm, "name").startsWith("hl7.fhir.r2.") || JSONUtil.str(npm, "name").startsWith("hl7.fhir.r2b.") || JSONUtil.str(npm, "name").startsWith("hl7.fhir.r3.") || JSONUtil.str(npm, "name").startsWith("hl7.fhir.r4.") || JSONUtil.str(npm, "name").startsWith("hl7.fhir.r5."))
496      return JSONUtil.str(npm, "version");
497    else {        
498      JsonObject dep = npm.getAsJsonObject("dependencies");
499      if (dep != null) {
500        for (Entry<String, JsonElement> e : dep.entrySet()) {
501          if (Utilities.existsInList(e.getKey(), "hl7.fhir.r2.core", "hl7.fhir.r2b.core", "hl7.fhir.r3.core", "hl7.fhir.r4.core"))
502            return e.getValue().getAsString();
503          if (Utilities.existsInList(e.getKey(), "hl7.fhir.core")) // while all packages are updated
504            return e.getValue().getAsString();
505        }
506      }
507      if (npm.has("fhirVersions")) {
508        return npm.getAsJsonArray("fhirVersions").get(0).getAsString();
509      }
510      if (dep != null) {
511        // legacy simplifier support:
512        if (dep.has("simplifier.core.r4"))
513          return "4.0";
514        if (dep.has("simplifier.core.r3"))
515          return "3.0";
516        if (dep.has("simplifier.core.r2"))
517          return "2.0";
518      }
519      throw new FHIRException("no core dependency or FHIR Version found in the Package definition");
520    }
521  }
522
523  public String summary() {
524    if (path != null)
525      return path;
526    else
527      return "memory";
528  }
529
530  public boolean isType(PackageType template) {
531    return template.getCode().equals(type());
532  }
533
534  public String type() {
535    return JSONUtil.str(npm, "type");
536  }
537
538  public String description() {
539    return JSONUtil.str(npm, "description");
540  }
541
542  public String getPath() {
543    return path;
544  }
545
546  public List<String> dependencies() {
547    List<String> res = new ArrayList<>();
548    if (npm.has("dependencies")) {
549      for (Entry<String, JsonElement> e : npm.getAsJsonObject("dependencies").entrySet()) {
550        res.add(e.getKey()+"#"+e.getValue().getAsString());
551      }
552    }
553    return res;
554  }
555
556  public String homepage() {
557    return JSONUtil.str(npm, "homepage");
558  }
559
560  public String url() {
561    return JSONUtil.str(npm, "url");
562  }
563
564
565  public String title() {
566    return JSONUtil.str(npm, "title");
567  }
568
569  public String toolsVersion() {
570    return JSONUtil.str(npm, "tools-version");
571  }
572
573  public String license() {
574    return JSONUtil.str(npm, "license");
575  }
576
577  //  /**
578  //   * only for use by the package manager itself
579  //   * 
580  //   * @param path
581  //   */
582  //  public void setPath(String path) {
583  //    this.path = path;
584  //  }
585
586  public String getWebLocation() {
587    if (npm.has("url")) {
588      return npm.get("url").getAsString();
589    } else {
590      return JSONUtil.str(npm, "canonical");
591    }
592  }
593
594  public InputStream loadResource(String type, String id) throws IOException {
595    NpmPackageFolder f = folders.get("package");
596    JsonArray files = f.index.getAsJsonArray("files");
597    for (JsonElement e : files) {
598      JsonObject i = (JsonObject) e;
599      if (type.equals(JSONUtil.str(i, "resourceType")) && id.equals(JSONUtil.str(i, "id"))) {
600        return load("package", JSONUtil.str(i, "filename"));
601      }
602    }
603    return null;
604  }
605
606  public InputStream loadExampleResource(String type, String id) throws IOException {
607    NpmPackageFolder f = folders.get("example");
608    if (f != null) {
609      JsonArray files = f.index.getAsJsonArray("files");
610      for (JsonElement e : files) {
611        JsonObject i = (JsonObject) e;
612        if (type.equals(JSONUtil.str(i, "resourceType")) && id.equals(JSONUtil.str(i, "id"))) {
613          return load("example", JSONUtil.str(i, "filename"));
614        }
615      }
616    }
617    return null;
618  }
619
620  /** special case when playing around inside the package **/
621  public Map<String, NpmPackageFolder> getFolders() {
622    return folders;
623  }
624
625  public void save(OutputStream stream) throws IOException {
626    TarArchiveOutputStream tar;
627    ByteArrayOutputStream OutputStream;
628    BufferedOutputStream bufferedOutputStream;
629    GzipCompressorOutputStream gzipOutputStream;
630
631    OutputStream = new ByteArrayOutputStream();
632    bufferedOutputStream = new BufferedOutputStream(OutputStream);
633    gzipOutputStream = new GzipCompressorOutputStream(bufferedOutputStream);
634    tar = new TarArchiveOutputStream(gzipOutputStream);
635
636
637    for (NpmPackageFolder folder : folders.values()) {
638      String n = folder.name;
639      if (!"package".equals(n)) {
640        n = "package/"+n;
641      }
642      NpmPackageIndexBuilder indexer = new NpmPackageIndexBuilder();
643      indexer.start();
644      for (String s : folder.content.keySet()) {
645        byte[] b = folder.content.get(s);
646        String name = n+"/"+s;
647        indexer.seeFile(s, b);
648        if (!s.equals(".index.json") && !s.equals("package.json")) {
649          TarArchiveEntry entry = new TarArchiveEntry(name);
650          entry.setSize(b.length);
651          tar.putArchiveEntry(entry);
652          tar.write(b);
653          tar.closeArchiveEntry();
654        }
655      }
656      byte[] cnt = indexer.build().getBytes(Charset.forName("UTF-8"));
657      TarArchiveEntry entry = new TarArchiveEntry(n+"/.index.json");
658      entry.setSize(cnt.length);
659      tar.putArchiveEntry(entry);
660      tar.write(cnt);
661      tar.closeArchiveEntry();
662    }
663    byte[] cnt = TextFile.stringToBytes(new GsonBuilder().setPrettyPrinting().create().toJson(npm), false);
664    TarArchiveEntry entry = new TarArchiveEntry("package/package.json");
665    entry.setSize(cnt.length);
666    tar.putArchiveEntry(entry);
667    tar.write(cnt);
668    tar.closeArchiveEntry();
669
670    tar.finish();
671    tar.close();
672    gzipOutputStream.close();
673    bufferedOutputStream.close();
674    OutputStream.close();
675    byte[] b = OutputStream.toByteArray();
676    stream.write(b);
677  }
678
679
680  public Map<String, List<String>> getTypes() {
681    return folders.get("package").types;
682  }
683
684  public String fhirVersionList() {
685    if (npm.has("fhirVersions")) {
686      CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
687      for (JsonElement n : npm.getAsJsonArray("fhirVersions")) {
688        b.append(n.getAsString());
689      }
690      return b.toString();
691    } else
692      return "";
693  }
694
695  public String dependencySummary() {
696    if (npm.has("dependencies")) {
697      CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
698      for (Entry<String, JsonElement> e : npm.getAsJsonObject("dependencies").entrySet()) {
699        b.append(e.getKey()+"#"+e.getValue().getAsString());
700      }
701      return b.toString();
702    } else
703      return "";
704  }
705
706  public void unPack(String dir) throws IOException {
707    unPack (dir, false);
708  }
709
710  public void unPackWithAppend(String dir) throws IOException {
711    unPack (dir, true);
712  }
713
714  public void unPack(String dir, boolean withAppend) throws IOException {
715    for (NpmPackageFolder folder : folders.values()) {
716      String dn = folder.getName();
717      if (!dn.equals("package") && (dn.startsWith("package/") || dn.startsWith("package\\"))) {
718        dn = dn.substring(8);
719      }
720      if (dn.equals("$root")) {
721        dn = dir;
722      } else {
723         dn = Utilities.path(dir, dn);
724      }
725      Utilities.createDirectory(dn);
726      for (String s : folder.listFiles()) {
727        String fn = Utilities.path(dn, s);
728        File f = new File(fn);
729        if (withAppend && f.getName().startsWith("_append.")) {
730          String appendFn = Utilities.path(dn, s.substring(8));
731          if (new File(appendFn).exists())
732            TextFile.appendBytesToFile(folder.fetchFile(s), appendFn);        
733          else
734            TextFile.bytesToFile(folder.fetchFile(s), appendFn);        
735        } else
736          TextFile.bytesToFile(folder.fetchFile(s), fn);
737      }
738//      if (path != null)
739//        FileUtils.copyDirectory(new File(path), new File(dir));      
740    }
741  }
742
743  public void debugDump(String purpose) {
744//    System.out.println("Debug Dump of Package for '"+purpose+"'. Path = "+path);
745//    System.out.println("  npm = "+name()+"#"+version()+", canonical = "+canonical());
746//    System.out.println("  folders = "+folders.size());
747//    for (String s : sorted(folders.keySet())) {
748//      NpmPackageFolder folder = folders.get(s);
749//      System.out.println("    "+folder.dump());
750//    }
751  }
752
753  private List<String> sorted(Set<String> keys) {
754    List<String> res = new ArrayList<String>();
755    res.addAll(keys);
756    Collections.sort(res);
757    return res ;
758  }
759}
760