source: trunk/abcl/src/org/armedbear/lisp/ZipCache.java

Last change on this file was 15494, checked in by Mark Evenson, 3 years ago

For zip archive from streams use modified date of byte source

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 21.1 KB
Line 
1/*
2 * ZipCache.java
3 *
4 * Copyright (C) 2010, 2014 Mark Evenson
5 * $Id: ZipCache.java 15494 2020-11-30 08:22:38Z mevenson $
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20 *
21 * As a special exception, the copyright holders of this library give you
22 * permission to link this library with independent modules to produce an
23 * executable, regardless of the license terms of these independent
24 * modules, and to copy and distribute the resulting executable under
25 * terms of your choice, provided that you also meet, for each linked
26 * independent module, the terms and conditions of the license of that
27 * module.  An independent module is a module which is not derived from
28 * or based on this library.  If you modify this library, you may extend
29 * this exception to your version of the library, but you are not
30 * obligated to do so.  If you do not wish to do so, delete this
31 * exception statement from your version.
32 */
33package org.armedbear.lisp;
34
35import java.io.ByteArrayInputStream;
36import java.io.ByteArrayOutputStream;
37import org.armedbear.lisp.util.HttpHead;
38import static org.armedbear.lisp.Lisp.*;
39
40import java.io.File;
41import java.io.IOException;
42import java.io.InputStream;
43import java.net.JarURLConnection;
44import java.net.MalformedURLException;
45import java.net.URL;
46import java.net.URLConnection;
47import java.text.ParsePosition;
48import java.text.SimpleDateFormat;
49import java.util.Date;
50import java.util.Enumeration;
51import java.util.Iterator;
52import java.util.HashMap;
53import java.util.Locale;
54import java.util.LinkedHashMap;
55import java.util.Map;
56import java.util.Set;
57import java.util.logging.Level;
58import java.util.logging.Logger;
59import java.util.zip.ZipException;
60import java.util.zip.ZipFile;
61import java.util.zip.ZipEntry;
62import java.util.zip.ZipInputStream;
63
64/**
65 * A cache for all zip/jar file access by JarPathname that uses the last
66 * modified time of the cached resource.
67 *
68 * If you run into problems with caching, use
69 * (SYS::DISABLE-ZIP-CACHE).  Once disabled, the caching cannot be
70 * re-enabled.
71 *
72 */ 
73public class ZipCache {
74  public static final boolean checkZipFile(Pathname name) {
75    InputStream input = name.getInputStream();
76    try {
77      byte[] bytes = new byte[4];
78      int bytesRead = input.read(bytes);
79      return bytesRead == 4 && bytes[0] == 80 && bytes[1] == 75 && bytes[2] == 3 && bytes[3] == 4;
80    } catch (Throwable t) {
81      // any error probably means 'no'
82      return false;
83    } finally {
84      if (input != null) {
85        try {
86          input.close();
87        } catch (IOException e) {
88        } // ignore exceptions
89      }
90    }
91  }
92  static InputStream getInputStream(ZipFile jarFile, String entryPath) {
93    ZipEntry entry = jarFile.getEntry(entryPath);
94    if (entry == null) {
95      Debug.trace("Failed to find entry " + "'" + entryPath + "'" + " in " + "'" + jarFile.getName() + "'");
96      return null;
97    }
98    InputStream result = null;
99    try {
100      result = jarFile.getInputStream(entry);
101    } catch (IOException e) {
102      Debug.trace("Failed to open InputStream for " + "'" + entryPath + "'" + " in " + "'" + jarFile.getName() + "'");
103      return null;
104    }
105    return result;
106  }
107  public static ZipInputStream getZipInputStream(ZipFile zipfile, String entryName) {
108    return ZipCache.getZipInputStream(zipfile, entryName, false);
109  }
110  public static ZipInputStream getZipInputStream(ZipFile zipfile, String entryName, boolean errorOnFailure) {
111    ZipEntry zipEntry = zipfile.getEntry(entryName);
112    ZipInputStream stream = null;
113    try {
114      stream = new ZipInputStream(zipfile.getInputStream(zipEntry));
115    } catch (IOException e) {
116      if (errorOnFailure) {
117        simple_error("Failed to open '" + entryName + "' in zipfile '" + zipfile + "': " + e.getMessage());
118      }
119      return null;
120    }
121    return stream;
122  }
123  public static ByteArrayOutputStream readEntry(ZipInputStream stream) {
124    ByteArrayOutputStream result = new ByteArrayOutputStream();
125    int count;
126    byte[] buf = new byte[1024]; // What's a decent buffer size?
127    try {
128      while ((count = stream.read(buf, 0, buf.length)) != -1) {
129        result.write(buf, 0, count);
130      }
131    } catch (IOException e) {
132      Debug.trace("Failed to read entry from " + stream + ": " + e);
133      return null;
134    }
135    return result;
136  }
137  public static ZipEntry getEntry(ZipInputStream zipInputStream, String entryName) {
138    return ZipCache.getEntry(zipInputStream, entryName, false);
139  }
140  public static ZipEntry getEntry(ZipInputStream zipInputStream, String entryName, boolean errorOnFailure) {
141    ZipEntry entry = null;
142    do {
143      try {
144        entry = zipInputStream.getNextEntry();
145      } catch (IOException e) {
146        if (errorOnFailure) {
147          Lisp.error(new FileError("Failed to seek for " + "'" + entryName + "'" + " in " + zipInputStream.toString()));
148        }
149        return null;
150      }
151    } while (entry != null && !entry.getName().equals(entryName));
152    if (entry != null) {
153      return entry;
154    }
155    if (errorOnFailure) {
156      Lisp.error(new FileError("Failed to find " + "'" + entryName + "'" + " in " + zipInputStream.toString()));
157    }
158    return null;
159  }
160
161  public static InputStream getEntryAsInputStream(ZipInputStream zipInputStream, String entryName) {
162    ZipEntry entry = getEntry(zipInputStream, entryName);
163    ByteArrayOutputStream bytes = readEntry(zipInputStream);
164    return new ByteArrayInputStream(bytes.toByteArray());
165  }
166
167  public static InputStream getEntryAsInputStream(JarPathname archiveEntry) {
168    JarPathname archiveJar = archiveEntry.getArchive();
169    Archive archive = ZipCache.getArchive(archiveJar);
170    InputStream result = archive.getEntryAsInputStream(archiveEntry);
171    if (result == null) {
172      simple_error("Failed to get InputStream for ~a", archiveEntry);
173    }
174    return result;
175  }
176
177  // To make this thread safe, we should return a proxy for ZipFile
178  // that keeps track of the number of outstanding references handed
179  // out, not allowing ZipFile.close() to succeed until that count
180  // has been reduced to 1 or the finalizer is executing.
181  // Unfortunately the relatively simple strategy of extending
182  // ZipFile via a CachedZipFile does not work because there is not
183  // a null arg constructor for ZipFile.
184  static HashMap<JarPathname, Archive> cache = new HashMap<JarPathname, Archive>();
185
186  abstract static public class Archive {
187    JarPathname root;
188    LinkedHashMap<JarPathname, ZipEntry> entries
189      = new LinkedHashMap<JarPathname, ZipEntry>();
190    long lastModified;
191
192    abstract InputStream getEntryAsInputStream(JarPathname entry);
193    abstract ZipEntry getEntry(JarPathname entry);
194    abstract void populateAllEntries();
195    abstract void close();
196    abstract long getLastModified();
197  }
198
199  static public class ArchiveStream
200    extends Archive
201  {
202    ZipInputStream source;
203    ZipEntry rootEntry;
204
205    public ArchiveStream(InputStream stream, JarPathname root, ZipEntry rootEntry) {
206      if (!(stream instanceof ZipInputStream)) {
207        this.source = new ZipInputStream(stream);
208      } else {
209        this.source = (ZipInputStream)stream;
210      }
211      this.root = root;
212      this.rootEntry = rootEntry;
213      this.lastModified = rootEntry.getTime(); // FIXME how to re-check time as modified?
214    } 
215
216    // TODO wrap in a weak reference to allow JVM to possibly reclaim memory
217    LinkedHashMap<JarPathname, ByteArrayOutputStream> contents
218      = new LinkedHashMap<JarPathname, ByteArrayOutputStream>();
219
220    boolean populated = false;
221
222    public InputStream getEntryAsInputStream(JarPathname entry) {
223      if (!populated) {
224        populateAllEntries();
225      }
226
227      entry.setVersion(Keyword.NEWEST);
228      ByteArrayOutputStream bytes = contents.get(entry);
229      if (bytes != null) {
230        return new ByteArrayInputStream(bytes.toByteArray());
231      }
232      return null;
233    }
234
235    public ZipEntry getEntry(JarPathname entry) {
236      if (!populated) {
237        populateAllEntries();
238      }
239      entry.setVersion(Keyword.NEWEST);
240      ZipEntry result = entries.get(entry);
241      return result;
242    }
243
244    void populateAllEntries() {
245      if (populated) {
246        return;
247      }
248      ZipEntry entry;
249      try {
250        while ((entry = source.getNextEntry()) != null) {
251          String name = entry.getName();
252          JarPathname entryPathname
253            = (JarPathname)JarPathname.createEntryFromJar(root, name);
254          entries.put(entryPathname, entry);
255          ByteArrayOutputStream bytes
256            = readEntry(source);
257          contents.put(entryPathname, bytes);
258        }
259        populated = true;
260      } catch (IOException e) {
261        simple_error("Failed to read entries from zip archive", root);
262      }
263    }
264
265    void close () {
266      if (source != null) {
267        try {
268          source.close();
269        } catch (IOException ex) {
270          {}
271        }
272      }
273    }
274
275    long getLastModified() {
276      return ((URLPathname)root.getRootJar()).getLastModified();
277    }
278  }
279
280  static public class ArchiveURL
281    extends ArchiveFile
282  {
283    JarURLConnection connection;
284
285    public ArchiveURL(JarPathname jar)
286      throws java.io.IOException
287    {
288      String rootJarURLString = jar.getRootJarAsURLString();
289      URL rootJarURL = new URL(rootJarURLString);
290      JarURLConnection jarConnection
291        = (JarURLConnection) rootJarURL.openConnection();
292
293      this.root = jar;
294      this.connection = jarConnection;
295      this.file = (ZipFile)connection.getJarFile();
296      this.lastModified = connection.getLastModified();
297    }
298   
299    void close() {
300      super.close();
301      // TODO: do we need to clean up from the connection?
302    }
303  }
304
305  static public class ArchiveFile
306    extends Archive
307  { 
308    ZipFile file;
309
310    ZipFile get() { return file;}
311
312    ArchiveFile() {}
313
314    public ArchiveFile(JarPathname jar)
315      throws ZipException, IOException
316    {
317      File f = ((Pathname)jar.getRootJar()).getFile();
318      this.root = jar;
319      this.file = new ZipFile(f);
320      this.lastModified = f.lastModified();
321    }
322
323    long getLastModified() {
324      long result = 0;
325
326      File f = ((Pathname)root.getRootJar()).getFile();
327      if (f != null) {
328        result = f.lastModified();
329      }
330      return result;
331    }
332
333    public ZipEntry getEntry(JarPathname entryPathname) {
334      entryPathname.setVersion(Keyword.NEWEST);
335      ZipEntry result = entries.get(entryPathname);
336      if (result != null) {
337        return result;
338      }
339      String entryPath = entryPathname.asEntryPath();
340      result = file.getEntry(entryPath);
341
342      if (result == null) {
343        return null;
344      }
345
346      // ZipFile.getEntry() will return directories when asked for
347      // files.
348      if (result.isDirectory()
349          && (!entryPathname.getName().equals(NIL)
350              || !entryPathname.getType().equals(NIL))) {
351        return null;
352      }
353
354      entries.put(entryPathname, result);
355      return result;
356    }
357
358    void populateAllEntries() {
359      ZipFile f = file;
360      if (f.size() == entries.size()) {
361        return;
362      }
363
364      Enumeration<? extends ZipEntry> e = f.entries();
365      while (e.hasMoreElements()) {
366        ZipEntry entry = e.nextElement();
367        String name = entry.getName();
368        JarPathname entryPathname
369          = (JarPathname)JarPathname.createEntryFromJar(root, name);
370        entries.put(entryPathname, entry);
371      }
372    }
373
374    InputStream getEntryAsInputStream(JarPathname entry) {
375      InputStream result = null;
376      entry.setVersion(Keyword.NEWEST);
377      ZipEntry zipEntry = getEntry(entry);
378
379      try { 
380        result = file.getInputStream(zipEntry);
381      } catch (IOException e) {} // FIXME how to signal a meaningful error?
382
383      return result;
384    }
385    void close() {
386      if (file != null) {
387        try {
388          file.close();
389        } catch (IOException e) {}
390         
391      }
392    }
393  }
394
395  static boolean cacheEnabled = true;
396  private final static Primitive DISABLE_ZIP_CACHE = new disable_zip_cache();
397  final static class disable_zip_cache extends Primitive {
398    disable_zip_cache() {
399      super("disable-zip-cache", PACKAGE_SYS, true, "",
400            "Not currently implemented");
401    }
402    @Override
403    public LispObject execute() {
404      return NIL;
405    }
406  }
407  static public synchronized void disable() {
408    cacheEnabled = false;
409    cache.clear(); 
410  }
411
412  synchronized public static LinkedHashMap<JarPathname,ZipEntry> getEntries(JarPathname jar) {
413    Archive archive = getArchive(jar);
414    archive.populateAllEntries(); // Very expensive for jars with large number of entries
415    return archive.entries;
416  }
417
418  synchronized public static Iterator<Map.Entry<JarPathname,ZipEntry>> getEntriesIterator(JarPathname jar) {
419    LinkedHashMap<JarPathname,ZipEntry> entries = getEntries(jar);
420    Set<Map.Entry<JarPathname,ZipEntry>> set = entries.entrySet();
421    return set.iterator();
422  }
423
424  static ZipEntry getZipEntry(JarPathname archiveEntry) {
425    JarPathname archiveJar = archiveEntry.getArchive();
426    Archive zip = getArchive(archiveJar);
427    ZipEntry entry = zip.getEntry(archiveEntry);
428    return entry;
429  }
430
431  // ??? we assume that DIRECTORY, NAME, and TYPE components are NIL
432  synchronized public static Archive getArchive(JarPathname jar) {
433    jar.setVersion(Keyword.NEWEST);
434    Archive result = cache.get(jar);
435    if (result != null) {
436      long time = result.getLastModified();
437      if (time != result.lastModified) {
438        cache.remove(jar);
439        return getArchive(jar);
440      }
441      return result;
442    }
443    Pathname rootJar = (Pathname) jar.getRootJar();
444    LispObject innerJars = jar.getJars().cdr();
445
446    if (!rootJar.isLocalFile()) {
447      return getArchiveURL(jar);
448    }
449   
450    if (innerJars.equals(NIL)) {
451      return getArchiveFile(jar);
452    } 
453
454    result = getArchiveStreamFromFile(jar);
455    cache.put(result.root, result); 
456
457    JarPathname nextArchive = new JarPathname();
458    nextArchive
459      .setDevice(new Cons(rootJar,
460                          new Cons(innerJars.car(), NIL)))
461      .setDirectory(NIL)
462      .setName(NIL)
463      .setType(NIL)
464      .setVersion(Keyword.NEWEST);
465     
466    innerJars = innerJars.cdr();
467    while (innerJars.car() != NIL) {
468      Pathname nextJarArchive = (Pathname)innerJars.car();
469     
470      JarPathname nextAsEntry = new JarPathname();
471      nextAsEntry
472        .setDevice(nextArchive.getDevice())
473        .setDirectory(nextJarArchive.getDirectory())
474        .setName(nextJarArchive.getName())
475        .setType(nextJarArchive.getType())
476        .setVersion(Keyword.NEWEST);
477      // FIXME
478      // The pathnames for subsquent entries in a PATHNAME-JAR
479      // are relative.  Should they be?
480      LispObject directories = nextAsEntry.getDirectory();
481      if ( !directories.equals(NIL)
482           && directories.car().equals(Keyword.RELATIVE)) {
483        directories = directories.cdr().push(Keyword.ABSOLUTE);
484        nextAsEntry.setDirectory(directories);
485      }
486
487      nextArchive.setDevice(nextArchive.getDevice().reverse().push(nextJarArchive).reverse());
488      ArchiveStream stream = (ArchiveStream) result;
489
490      ZipEntry entry = stream.getEntry(nextAsEntry);
491      if (entry == null) {
492        return null;
493      }
494     
495      InputStream inputStream = stream.getEntryAsInputStream(nextAsEntry);
496      if (inputStream == null) {
497        return null;
498      }
499      stream = new ArchiveStream(inputStream, nextArchive, entry);
500      result = stream;
501      cache.put(nextArchive, result); 
502
503      innerJars = innerJars.cdr();
504      if (innerJars.cdr().equals(NIL)
505          && (!jar.getDirectory().equals(NIL)
506              && jar.getName().equals(NIL)
507              && jar.getType().equals(NIL))) {
508        simple_error("Currently unimplemented retrieval of an entry in a nested pathnames");
509        return (Archive)UNREACHED;
510      }
511    }
512    return result;
513  }
514
515  static ArchiveStream getArchiveStreamFromFile(JarPathname p) {
516        JarPathname innerArchiveAsEntry = JarPathname.archiveAsEntry(p);
517    JarPathname root = new JarPathname();
518    root = (JarPathname)root.copyFrom(innerArchiveAsEntry);
519    root
520      .setDirectory(NIL)
521      .setName(NIL)
522      .setType(NIL)
523      .setVersion(Keyword.NEWEST);
524   
525    ArchiveFile rootArchiveFile = (ArchiveFile)getArchiveFile(root);
526    ZipEntry entry = rootArchiveFile.getEntry(innerArchiveAsEntry);
527    if (entry == null) {
528      return null;
529    }
530    InputStream inputStream = rootArchiveFile.getEntryAsInputStream(innerArchiveAsEntry);
531    if (inputStream == null) {
532      return null;
533    }
534    ArchiveStream result = new ArchiveStream(inputStream, p, entry);
535    return result;
536  }
537
538  public static Archive getArchiveURL(JarPathname jar) {
539    Pathname rootJar = (Pathname) jar.getRootJar();
540    jar.setVersion(Keyword.NEWEST);
541
542    URL rootJarURL = null;
543    try {
544      ArchiveURL result = new ArchiveURL(jar);
545      cache.put(jar, result);
546      return result;
547    } catch (MalformedURLException e) {
548      simple_error("Failed to form root URL for ~a", jar);
549      return (Archive)UNREACHED;     
550    } catch (IOException e) {
551      simple_error("Failed to fetch ~a: ~a", jar, e);
552      return (Archive)UNREACHED;     
553    }
554  }
555
556  static public Archive getArchiveFile(JarPathname jar) {
557    jar.setVersion(Keyword.NEWEST);
558    try {
559      ArchiveFile result = new ArchiveFile(jar);
560      cache.put(jar, result);
561      return result;
562    } catch (ZipException e) {
563      error(new FileError("Failed to open local zip archive"
564                          + " because " + e, jar));
565                         
566      return (Archive)UNREACHED;
567    } catch (IOException e) {
568      error(new FileError("Failed to open local zip archive"
569                          + " because " + e, jar));
570      return (Archive)UNREACHED;
571    }
572  }
573
574  // unused
575  static void checkRemoteLastModified(ArchiveURL archive) {
576    // Unfortunately, the Apple JDK under OS X doesn't do
577    // HTTP HEAD requests, instead refetching the entire
578    // resource, and I assume this is the case in all
579    // Sun-derived JVMs.  So, we use a custom HEAD
580    // implementation only looking for Last-Modified
581    // headers, which if we don't find, we give up and
582    // refetch the resource.
583
584    String dateString = null;
585
586    String url = archive.root.getRootJarAsURLString();
587   
588    try {
589      dateString = HttpHead.get(url, "Last-Modified");
590    } catch (IOException ex) {
591      Debug.trace(ex);
592    }
593    Date date = null;
594    ParsePosition pos = new ParsePosition(0);
595
596    final SimpleDateFormat ASCTIME
597      = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy", Locale.US);
598    final SimpleDateFormat RFC_1036
599      = new SimpleDateFormat("EEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US);
600    final SimpleDateFormat RFC_1123
601      = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
602
603    if (dateString != null) {
604      date = RFC_1123.parse(dateString, pos);
605      if (date == null) {
606        date = RFC_1036.parse(dateString, pos);
607        if (date == null) {
608          date = ASCTIME.parse(dateString, pos);
609        }
610      }
611    }
612
613    // Replace older item in cache
614    if (date == null || date.getTime() > archive.lastModified) {
615      JarPathname root = archive.root;
616      Archive entry = getArchiveURL(root);
617      cache.put(root, entry);
618    }
619    if (date == null) {
620      if (dateString == null) {
621        Debug.trace("Failed to retrieve request header: "
622                    + url.toString());
623      } else {
624        Debug.trace("Failed to parse Last-Modified date: " +
625                    dateString);
626      }
627    }
628  }
629
630  // ## clear-zip-cache  => boolean
631  private static final Primitive CLEAR_ZIP_CACHE = new clear_zip_cache();
632  private static class clear_zip_cache extends Primitive { 
633    clear_zip_cache() {
634      super("clear-zip-cache", PACKAGE_SYS, true);
635    }
636    @Override
637    public LispObject execute() {
638      int size = cache.size();
639      cache.clear();
640      return size == 0 ? NIL : T;
641    }
642  }
643
644  // ## remove-zip-cache-entry pathname => boolean
645  private static final Primitive REMOVE_ZIP_CACHE_ENTRY = new remove_zip_cache_entry();
646  private static class remove_zip_cache_entry extends Primitive { 
647    remove_zip_cache_entry() {
648      super("remove-zip-cache-entry", PACKAGE_SYS, true, "pathname");
649    }
650    @Override
651    public LispObject execute(LispObject arg) {
652      Pathname p = coerceToPathname(arg);
653      boolean result = false;
654      if (p instanceof JarPathname) {
655        result = ZipCache.remove((JarPathname)p);
656      } 
657      return result ? T : NIL;
658    }
659  }
660
661  synchronized public static boolean remove(Pathname pathname) {
662    JarPathname p = JarPathname.createFromPathname(pathname);
663    return remove(p);
664  }
665     
666  synchronized public static boolean remove(JarPathname p) {
667    p.setVersion(Keyword.NEWEST);
668    Archive archive = cache.get(p);
669    if (archive != null) {
670      archive.close();
671      cache.remove(p);
672      return true;
673    }
674    return false;
675  }
676}
677
Note: See TracBrowser for help on using the repository browser.