/* * Copyright (c) 1998-2010 Caucho Technology -- all rights reserved * * This file is part of Resin(R) Open Source * * Each copy or derived work must preserve the copyright notice and this * notice unmodified. * * Resin Open Source is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * Resin Open Source is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty * of NON-INFRINGEMENT. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with Resin Open Source; if not, write to the * * Free Software Foundation, Inc. * 59 Temple Place, Suite 330 * Boston, MA 02111-1307 USA * * @author Scott Ferguson */ package com.caucho.vfs; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.ref.SoftReference; import java.security.cert.Certificate; import java.util.ArrayList; import java.util.Iterator; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.logging.Level; import java.util.logging.Logger; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import com.caucho.loader.EnvironmentLocal; import com.caucho.make.CachedDependency; import com.caucho.util.Alarm; import com.caucho.util.CacheListener; import com.caucho.util.L10N; import com.caucho.util.Log; import com.caucho.util.LruCache; /** * Jar is a cache around a jar file to avoid scanning through the whole * file on each request. * * <p>When the Jar is created, it scans the file and builds a directory * of the Jar entries. */ public class Jar implements CacheListener { private static final Logger log = Log.open(Jar.class); private static final L10N L = new L10N(Jar.class); private static LruCache<Path,Jar> _jarCache; private static EnvironmentLocal<Integer> _jarSize = new EnvironmentLocal<Integer>("caucho.vfs.jar-size"); private static ZipEntry NULL_ZIP = new ZipEntry("null"); private LruCache<String,ZipEntry> _zipEntryCache = new LruCache<String,ZipEntry>(64); private Path _backing; private boolean _backingIsFile; private AtomicInteger _changeSequence = new AtomicInteger(); private JarDepend _depend; // saved last modified time private long _lastModified; // saved length private long _length; // last time the file was checked private long _lastTime; // cached zip file to read jar entries private SoftReference<JarFile> _jarFileRef; // last time the zip file was modified private long _jarLength; private Boolean _isSigned; // file to be closed private final AtomicReference<SoftReference<JarFile>> _closeJarFileRef = new AtomicReference<SoftReference<JarFile>>(); /** * Creates a new Jar. * * @param path canonical path */ private Jar(Path backing) { if (backing instanceof JarPath) throw new IllegalStateException(); _backing = backing; _backingIsFile = (_backing.getScheme().equals("file") && _backing.canRead()); } /** * Return a Jar for the path. If the backing already exists, return * the old jar. */ static Jar create(Path backing) { if (_jarCache == null) { int size = 256; Integer iSize = _jarSize.get(); if (iSize != null) size = iSize.intValue(); _jarCache = new LruCache<Path,Jar>(size); } Jar jar = _jarCache.get(backing); if (jar == null) { jar = new Jar(backing); jar = _jarCache.putIfNew(backing, jar); } return jar; } /** * Return a Jar for the path. If the backing already exists, return * the old jar. */ static Jar getJar(Path backing) { if (_jarCache != null) { Jar jar = _jarCache.get(backing); return jar; } return null; } /** * Return a Jar for the path. If the backing already exists, return * the old jar. */ public static PersistentDependency createDepend(Path backing) { Jar jar = create(backing); return jar.getDepend(); } /** * Return a Jar for the path. If the backing already exists, return * the old jar. */ public static PersistentDependency createDepend(Path backing, long digest) { Jar jar = create(backing); return new JarDigestDepend(jar.getJarDepend(), digest); } /** * Returns the backing path. */ Path getBacking() { return _backing; } /** * Returns the dependency. */ public PersistentDependency getDepend() { return getJarDepend(); } /** * Returns the dependency. */ private JarDepend getJarDepend() { if (_depend == null || _depend.isModified()) _depend = new JarDepend(new Depend(getBacking())); return _depend; } public int getChangeSequence() { return _changeSequence.get(); } private boolean isSigned() { Boolean isSigned = _isSigned; if (isSigned != null) return isSigned; try { Manifest manifest = getManifest(); if (manifest == null) { _isSigned = Boolean.FALSE; return false; } Map<String,Attributes> entries = manifest.getEntries(); if (entries == null) { _isSigned = Boolean.FALSE; return false; } for (Attributes attr : entries.values()) { for (Object key : attr.keySet()) { String keyString = String.valueOf(key); if (keyString.contains("Digest")) { _isSigned = Boolean.TRUE; return true; } } } } catch (IOException e) { log.log(Level.FINE, e.toString(), e); } _isSigned = Boolean.FALSE; return false; } /** * Returns true if the entry is a file in the jar. * * @param path the path name inside the jar. */ public Manifest getManifest() throws IOException { Manifest manifest; synchronized (this) { JarFile jarFile = getJarFile(); if (jarFile == null) manifest = null; else manifest = jarFile.getManifest(); } closeJarFile(); return manifest; } /** * Returns any certificates. */ public Certificate []getCertificates(String path) { if (! isSigned()) return null; if (path.length() > 0 && path.charAt(0) == '/') path = path.substring(1); try { if (! _backing.canRead()) return null; JarFile jarFile = new JarFile(_backing.getNativePath()); JarEntry entry; InputStream is = null; try { entry = jarFile.getJarEntry(path); if (entry != null) { is = jarFile.getInputStream(entry); while (is.skip(65536) > 0) { } is.close(); return entry.getCertificates(); } } finally { jarFile.close(); } } catch (IOException e) { log.log(Level.FINE, e.toString(), e); return null; } return null; } /** * Returns true if the entry exists in the jar. * * @param path the path name inside the jar. */ public boolean exists(String path) { // server/249f, server/249g // XXX: facelets vs issue of meta-inf (i.e. lower case) try { ZipEntry entry = getJarEntry(path); return entry != null; } catch (IOException e) { log.log(Level.FINE, e.toString(), e); } return false; } /** * Returns true if the entry is a directory in the jar. * * @param path the path name inside the jar. */ public boolean isDirectory(String path) { boolean result = false; try { ZipEntry entry = getJarEntry(path); return entry != null && entry.isDirectory(); } catch (IOException e) { log.log(Level.FINE, e.toString(), e); } return false; } /** * Returns true if the entry is a file in the jar. * * @param path the path name inside the jar. */ public boolean isFile(String path) { try { ZipEntry entry = getJarEntry(path); return entry != null && ! entry.isDirectory(); } catch (IOException e) { log.log(Level.FINE, e.toString(), e); } return false; } /** * Returns the last-modified time of the entry in the jar file. * * @param path full path to the jar entry * @return the length of the entry */ public long getLastModified(String path) { try { // this entry time can cause problems ... ZipEntry entry = getJarEntry(path); return entry != null ? entry.getTime() : -1; } catch (IOException e) { log.log(Level.FINE, e.toString(), e); } return -1; } /** * Returns the length of the entry in the jar file. * * @param path full path to the jar entry * @return the length of the entry */ public long getLength(String path) { try { ZipEntry entry = getJarEntry(path); long length = entry != null ? entry.getSize() : -1; return length; } catch (IOException e) { log.log(Level.FINE, e.toString(), e); return -1; } } /** * Readable if the jar is readable and the path refers to a file. */ public boolean canRead(String path) { try { ZipEntry entry = getJarEntry(path); return entry != null && ! entry.isDirectory(); } catch (IOException e) { log.log(Level.FINE, e.toString(), e); return false; } } /** * Can't write to jars. */ public boolean canWrite(String path) { return false; } /** * Lists all the files in this directory. */ public String []list(String path) throws IOException { // XXX: return new String[0]; } /** * Opens a stream to an entry in the jar. * * @param path relative path into the jar. */ public StreamImpl openReadImpl(Path path) throws IOException { String pathName = path.getPath(); if (pathName.length() > 0 && pathName.charAt(0) == '/') pathName = pathName.substring(1); ZipFile zipFile = new ZipFile(_backing.getNativePath()); ZipEntry entry; InputStream is = null; try { entry = zipFile.getEntry(pathName); if (entry != null) { is = zipFile.getInputStream(entry); return new ZipStreamImpl(zipFile, is, null, path); } else { throw new FileNotFoundException(path.toString()); } } finally { if (is == null) { zipFile.close(); } } } /** * Clears any cached JarFile. */ public void clearCache() { JarFile jarFile = null; synchronized (this) { SoftReference<JarFile> jarFileRef = _jarFileRef; _jarFileRef = null; if (jarFileRef != null) jarFile = jarFileRef.get(); } try { if (jarFile != null) jarFile.close(); } catch (Exception e) { } } private ZipEntry getJarEntry(String path) throws IOException { ZipEntry entry = _zipEntryCache.get(path); if (entry != null && isCacheValid()) { if (entry == NULL_ZIP) return null; else return entry; } synchronized (this) { entry = getJarEntryImpl(path); } closeJarFile(); if (entry != null) { _zipEntryCache.put(path, entry); } else { _zipEntryCache.put(path, NULL_ZIP); } return entry; } private ZipEntry getJarEntryImpl(String path) throws IOException { if (path.startsWith("/")) path = path.substring(1); JarFile jarFile = getJarFile(); if (jarFile != null) return jarFile.getJarEntry(path); else return null; } /** * Returns the Java ZipFile for this Jar. Accessing the entries with * the ZipFile is faster than scanning through them. * * getJarFile is not thread safe. */ private JarFile getJarFile() throws IOException { JarFile jarFile = null; isCacheValid(); SoftReference<JarFile> jarFileRef = _jarFileRef; if (jarFileRef != null) { jarFile = jarFileRef.get(); if (jarFile != null) return jarFile; } SoftReference<JarFile> oldJarRef = _jarFileRef; _jarFileRef = null; JarFile oldFile = null; if (oldJarRef == null) { } else if (_closeJarFileRef.compareAndSet(null, oldJarRef)) { } else oldFile = oldJarRef.get(); if (oldFile != null) { try { oldFile.close(); } catch (Throwable e) { e.printStackTrace(); } } if (_backingIsFile) { try { jarFile = new JarFile(_backing.getNativePath()); } catch (IOException ex) { if (log.isLoggable(Level.FINE)) log.log(Level.FINE, L.l("Error opening jar file '{0}'", _backing.getNativePath())); throw ex; } _jarFileRef = new SoftReference<JarFile>(jarFile); getLastModifiedImpl(); } return jarFile; } /** * Returns the last modified time for the path. * * @param path path into the jar. * * @return the last modified time of the jar in milliseconds. */ private long getLastModifiedImpl() { isCacheValid(); return _lastModified; } /** * Returns the last modified time for the path. * * @param path path into the jar. * * @return the last modified time of the jar in milliseconds. */ private boolean isCacheValid() { long now = Alarm.getCurrentTime(); if ((now - _lastTime < 100) && ! Alarm.isTest()) return true; long oldLastModified = _lastModified; long oldLength = _length; long newLastModified = _backing.getLastModified(); long newLength = _backing.getLength(); _lastTime = now; if (newLastModified == oldLastModified && newLength == oldLength) { _lastTime = now; return true; } else { _changeSequence.incrementAndGet(); // If the file has changed, close the old file SoftReference<JarFile> oldFileRef = _jarFileRef; _jarFileRef = null; _depend = null; _isSigned = null; _zipEntryCache.clear(); _lastModified = newLastModified; _length = newLength; _lastTime = now; SoftReference<JarFile> oldCloseFileRef = null; oldCloseFileRef = _closeJarFileRef.getAndSet(oldFileRef); if (oldCloseFileRef != null) { JarFile oldCloseFile = oldCloseFileRef.get(); try { if (oldCloseFile != null) oldCloseFile.close(); } catch (Throwable e) { } } return false; } } /** * Closes any old jar waiting for close. */ private void closeJarFile() { SoftReference<JarFile> jarFileRef = _closeJarFileRef.getAndSet(null); if (jarFileRef != null) { JarFile jarFile = jarFileRef.get(); if (jarFile != null) { try { jarFile.close(); } catch (IOException e) { log.log(Level.WARNING, e.toString(), e); } } } } public void close() { removeEvent(); } @Override public void removeEvent() { JarFile jarFile = null; synchronized (this) { if (_jarFileRef != null) jarFile = _jarFileRef.get(); _jarFileRef = null; } try { if (jarFile != null) jarFile.close(); } catch (Throwable e) { log.log(Level.FINE, e.toString(), e); } closeJarFile(); } public boolean equals(Object o) { if (this == o) return true; else if (o == null || ! getClass().equals(o.getClass())) return false; Jar jar = (Jar) o; return _backing.equals(jar._backing); } /** * Clears all the cached files in the jar. Needed to avoid some * windows NT issues. */ public static void clearJarCache() { LruCache<Path,Jar> jarCache = _jarCache; if (jarCache == null) return; ArrayList<Jar> jars = new ArrayList<Jar>(); synchronized (jarCache) { Iterator<Jar> iter = jarCache.values(); while (iter.hasNext()) jars.add(iter.next()); } for (int i = 0; i < jars.size(); i++) { Jar jar = jars.get(i); if (jar != null) jar.clearCache(); } } @Override public String toString() { return _backing.toString(); } /** * StreamImpl to read from a ZIP file. */ static class ZipStreamImpl extends StreamImpl { private ZipFile _zipFile; private InputStream _zis; private InputStream _is; /** * Create the new stream impl. * * @param zis the underlying zip stream. * @param is the backing stream. * @param path the path to the jar entry. */ ZipStreamImpl(ZipFile file, InputStream zis, InputStream is, Path path) { _zipFile = file; _zis = zis; _is = is; setPath(path); } /** * Returns true since this is a read stream. */ public boolean canRead() { return true; } public int getAvailable() throws IOException { if (_zis == null) return -1; else return _zis.available(); } public int read(byte []buf, int off, int len) throws IOException { int readLen = _zis.read(buf, off, len); return readLen; } public void close() throws IOException { ZipFile zipFile = _zipFile; _zipFile = null; InputStream zis = _zis; _zis = null; InputStream is = _is; _is = null; try { if (zis != null) zis.close(); } catch (Throwable e) { } try { if (zipFile != null) zipFile.close(); } catch (Throwable e) { } if (is != null) is.close(); } protected void finalize() throws IOException { close(); } } class JarDepend extends CachedDependency implements PersistentDependency { private Depend _depend; private boolean _isDigestModified; /** * Create a new dependency. * * @param source the source file */ JarDepend(Depend depend) { _depend = depend; } /** * Create a new dependency. * * @param source the source file */ JarDepend(Depend depend, long digest) { _depend = depend; _isDigestModified = _depend.getDigest() != digest; } /** * Returns the underlying depend. */ Depend getDepend() { return _depend; } /** * Returns true if the dependency is modified. */ @Override public boolean isModifiedImpl() { if (_isDigestModified || _depend.isModified()) { _changeSequence.incrementAndGet(); return true; } else return false; } /** * Returns true if the dependency is modified. */ @Override public boolean logModified(Logger log) { return _depend.logModified(log); } /** * Returns the string to recreate the Dependency. */ @Override public String getJavaCreateString() { String sourcePath = _depend.getPath().getPath(); long digest = _depend.getDigest(); return ("new com.caucho.vfs.Jar.createDepend(" + "com.caucho.vfs.Vfs.lookup(\"" + sourcePath + "\"), " + digest + "L)"); } public String toString() { return "Jar$JarDepend[" + _depend.getPath() + "]"; } } static class JarDigestDepend implements PersistentDependency { private JarDepend _jarDepend; private Depend _depend; private boolean _isDigestModified; /** * Create a new dependency. * * @param source the source file */ JarDigestDepend(JarDepend jarDepend, long digest) { _jarDepend = jarDepend; _depend = jarDepend.getDepend(); _isDigestModified = _depend.getDigest() != digest; } /** * Returns true if the dependency is modified. */ public boolean isModified() { return _isDigestModified || _jarDepend.isModified(); } /** * Returns true if the dependency is modified. */ public boolean logModified(Logger log) { return _depend.logModified(log) || _jarDepend.logModified(log); } /** * Returns the string to recreate the Dependency. */ public String getJavaCreateString() { String sourcePath = _depend.getPath().getPath(); long digest = _depend.getDigest(); return ("new com.caucho.vfs.Jar.createDepend(" + "com.caucho.vfs.Vfs.lookup(\"" + sourcePath + "\"), " + digest + "L)"); } } }