Enable limits on the number of open files by the bundle cache. (FELIX-3071)


git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1161751 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/framework/src/main/java/org/apache/felix/framework/cache/BundleArchive.java b/framework/src/main/java/org/apache/felix/framework/cache/BundleArchive.java
index a09b96d..7829e42 100644
--- a/framework/src/main/java/org/apache/felix/framework/cache/BundleArchive.java
+++ b/framework/src/main/java/org/apache/felix/framework/cache/BundleArchive.java
@@ -24,6 +24,7 @@
 import java.util.SortedMap;
 import java.util.TreeMap;
 import org.apache.felix.framework.Logger;
+import org.apache.felix.framework.util.WeakZipFileFactory;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.Constants;
 
@@ -77,6 +78,7 @@
 
     private final Logger m_logger;
     private final Map m_configMap;
+    private final WeakZipFileFactory m_zipFactory;
     private final File m_archiveRootDir;
     private final boolean m_isSingleBundleFile;
 
@@ -121,11 +123,13 @@
      * @param is input stream from which to read the bundle content.
      * @throws Exception if any error occurs.
     **/
-    public BundleArchive(Logger logger, Map configMap, File archiveRootDir, long id,
-        int startLevel, String location, InputStream is) throws Exception
+    public BundleArchive(Logger logger, Map configMap, WeakZipFileFactory zipFactory,
+        File archiveRootDir, long id, int startLevel, String location, InputStream is)
+        throws Exception
     {
         m_logger = logger;
         m_configMap = configMap;
+        m_zipFactory = zipFactory;
         m_archiveRootDir = archiveRootDir;
         m_id = id;
         if (m_id <= 0)
@@ -161,11 +165,13 @@
      * @param configMap configMap for BundleArchive
      * @throws Exception if any error occurs.
     **/
-    public BundleArchive(Logger logger, Map configMap, File archiveRootDir)
+    public BundleArchive(Logger logger, Map configMap, WeakZipFileFactory zipFactory,
+        File archiveRootDir)
         throws Exception
     {
         m_logger = logger;
         m_configMap = configMap;
+        m_zipFactory = zipFactory;
         m_archiveRootDir = archiveRootDir;
 
         String s = (String) m_configMap.get(BundleCache.CACHE_SINGLEBUNDLEFILE_PROP);
@@ -852,25 +858,25 @@
                 if (BundleCache.getSecureAction().isFileDirectory(file))
                 {
                     result = new DirectoryRevision(m_logger, m_configMap,
-                        revisionRootDir, location);
+                        m_zipFactory, revisionRootDir, location);
                 }
                 else
                 {
-                    result = new JarRevision(m_logger, m_configMap, revisionRootDir,
-                        location, true);
+                    result = new JarRevision(m_logger, m_configMap,
+                        m_zipFactory, revisionRootDir, location, true, null);
                 }
             }
             else if (location.startsWith(INPUTSTREAM_PROTOCOL))
             {
                 // Assume all input streams point to JAR files.
-                result = new JarRevision(m_logger, m_configMap, revisionRootDir,
-                    location, false, is);
+                result = new JarRevision(m_logger, m_configMap,
+                    m_zipFactory, revisionRootDir, location, false, is);
             }
             else
             {
                 // Anything else is assumed to be a URL to a JAR file.
-                result = new JarRevision(m_logger, m_configMap, revisionRootDir,
-                    location, false);
+                result = new JarRevision(m_logger, m_configMap,
+                    m_zipFactory, revisionRootDir, location, false, null);
             }
         }
         catch (Exception ex)
diff --git a/framework/src/main/java/org/apache/felix/framework/cache/BundleCache.java b/framework/src/main/java/org/apache/felix/framework/cache/BundleCache.java
index 9505425..ff79158 100644
--- a/framework/src/main/java/org/apache/felix/framework/cache/BundleCache.java
+++ b/framework/src/main/java/org/apache/felix/framework/cache/BundleCache.java
@@ -25,6 +25,7 @@
 
 import org.apache.felix.framework.Logger;
 import org.apache.felix.framework.util.SecureAction;
+import org.apache.felix.framework.util.WeakZipFileFactory;
 import org.osgi.framework.Constants;
 
 /**
@@ -74,19 +75,22 @@
     public static final String CACHE_BUFSIZE_PROP = "felix.cache.bufsize";
     public static final String CACHE_ROOTDIR_PROP = "felix.cache.rootdir";
     public static final String CACHE_LOCKING_PROP = "felix.cache.locking";
+    public static final String CACHE_FILELIMIT_PROP = "felix.cache.filelimit";
     // TODO: CACHE - Remove this once we migrate the cache format.
     public static final String CACHE_SINGLEBUNDLEFILE_PROP = "felix.cache.singlebundlefile";
 
     protected static transient int BUFSIZE = 4096;
-    protected static transient final String CACHE_DIR_NAME = "felix-cache";
-    protected static transient final String CACHE_ROOTDIR_DEFAULT = ".";
-    protected static transient final String CACHE_LOCK_NAME = "cache.lock";
-    protected static transient final String BUNDLE_DIR_PREFIX = "bundle";
+
+    private static transient final String CACHE_DIR_NAME = "felix-cache";
+    private static transient final String CACHE_ROOTDIR_DEFAULT = ".";
+    private static transient final String CACHE_LOCK_NAME = "cache.lock";
+    static transient final String BUNDLE_DIR_PREFIX = "bundle";
 
     private static final SecureAction m_secureAction = new SecureAction();
 
     private final Logger m_logger;
     private final Map m_configMap;
+    private final WeakZipFileFactory m_zipFactory;
     private final Object m_lock;
 
     public BundleCache(Logger logger, Map configMap)
@@ -95,6 +99,19 @@
         m_logger = logger;
         m_configMap = configMap;
 
+        String limitStr = (String) m_configMap.get(CACHE_FILELIMIT_PROP);
+        limitStr = (limitStr == null) ? "10" : limitStr;
+        int limit;
+        try
+        {
+            limit = Integer.parseInt(limitStr);
+        }
+        catch (NumberFormatException ex)
+        {
+            limit = 10;
+        }
+        m_zipFactory = new WeakZipFileFactory(limit);
+
         // Create the cache directory, if it does not exist.
         File cacheDir = determineCacheDir(m_configMap);
         if (!getSecureAction().fileExists(cacheDir))
@@ -220,7 +237,9 @@
                 // Recreate the bundle archive.
                 try
                 {
-                    archiveList.add(new BundleArchive(m_logger, m_configMap, children[i]));
+                    archiveList.add(
+                        new BundleArchive(
+                            m_logger, m_configMap, m_zipFactory, children[i]));
                 }
                 catch (Exception ex)
                 {
@@ -250,7 +269,8 @@
             // Create the archive and add it to the list of archives.
             BundleArchive ba =
                 new BundleArchive(
-                    m_logger, m_configMap, archiveRootDir, id, startLevel, location, is);
+                    m_logger, m_configMap, m_zipFactory, archiveRootDir,
+                    id, startLevel, location, is);
             return ba;
         }
         catch (Exception ex)
@@ -346,8 +366,8 @@
             // We might be talking windows and native libs -- hence,
             // try to trigger a gc and try again. The hope is that
             // this releases the classloader that loaded the native
-            // lib and allows us to delete it because it then 
-            // would not be used anymore. 
+            // lib and allows us to delete it because it then
+            // would not be used anymore.
             System.gc();
             System.gc();
             return deleteDirectoryTreeRecursive(target);
diff --git a/framework/src/main/java/org/apache/felix/framework/cache/DirectoryContent.java b/framework/src/main/java/org/apache/felix/framework/cache/DirectoryContent.java
index 8e2af9c..b6b5b54 100644
--- a/framework/src/main/java/org/apache/felix/framework/cache/DirectoryContent.java
+++ b/framework/src/main/java/org/apache/felix/framework/cache/DirectoryContent.java
@@ -25,6 +25,7 @@
 import org.apache.felix.framework.Logger;
 import org.apache.felix.framework.util.FelixConstants;
 import org.apache.felix.framework.util.Util;
+import org.apache.felix.framework.util.WeakZipFileFactory;
 import org.osgi.framework.Constants;
 
 public class DirectoryContent implements Content
@@ -35,16 +36,18 @@
 
     private final Logger m_logger;
     private final Map m_configMap;
+    private final WeakZipFileFactory m_zipFactory;
     private final Object m_revisionLock;
     private final File m_rootDir;
     private final File m_dir;
     private Map m_nativeLibMap;
 
-    public DirectoryContent(Logger logger, Map configMap, Object revisionLock,
-        File rootDir, File dir)
+    public DirectoryContent(Logger logger, Map configMap,
+        WeakZipFileFactory zipFactory, Object revisionLock, File rootDir, File dir)
     {
         m_logger = logger;
         m_configMap = configMap;
+        m_zipFactory = zipFactory;
         m_revisionLock = revisionLock;
         m_rootDir = rootDir;
         m_dir = dir;
@@ -163,7 +166,7 @@
         if (entryName.equals(FelixConstants.CLASS_PATH_DOT))
         {
             return new DirectoryContent(
-                m_logger, m_configMap, m_revisionLock, m_rootDir, m_dir);
+                m_logger, m_configMap, m_zipFactory, m_revisionLock, m_rootDir, m_dir);
         }
 
         // Remove any leading slash, since all bundle class path
@@ -180,7 +183,7 @@
         if (BundleCache.getSecureAction().isFileDirectory(file))
         {
             return new DirectoryContent(
-                m_logger, m_configMap, m_revisionLock, m_rootDir, file);
+                m_logger, m_configMap, m_zipFactory, m_revisionLock, m_rootDir, file);
         }
         else if (BundleCache.getSecureAction().fileExists(file)
             && entryName.endsWith(".jar"))
@@ -202,7 +205,8 @@
                 }
             }
             return new JarContent(
-                m_logger, m_configMap, m_revisionLock, extractDir, file, null);
+                m_logger, m_configMap, m_zipFactory, m_revisionLock,
+                extractDir, file, null);
         }
 
         // The entry could not be found, so return null.
diff --git a/framework/src/main/java/org/apache/felix/framework/cache/DirectoryRevision.java b/framework/src/main/java/org/apache/felix/framework/cache/DirectoryRevision.java
index efaf855..c1269c0 100644
--- a/framework/src/main/java/org/apache/felix/framework/cache/DirectoryRevision.java
+++ b/framework/src/main/java/org/apache/felix/framework/cache/DirectoryRevision.java
@@ -26,6 +26,7 @@
 
 import org.apache.felix.framework.Logger;
 import org.apache.felix.framework.util.StringMap;
+import org.apache.felix.framework.util.WeakZipFileFactory;
 
 /**
  * <p>
@@ -36,12 +37,15 @@
 **/
 class DirectoryRevision extends BundleArchiveRevision
 {
+    private final WeakZipFileFactory m_zipFactory;
     private final File m_refDir;
 
     public DirectoryRevision(
-        Logger logger, Map configMap, File revisionRootDir, String location) throws Exception
+        Logger logger, Map configMap, WeakZipFileFactory zipFactory,
+        File revisionRootDir, String location) throws Exception
     {
         super(logger, configMap, revisionRootDir, location);
+        m_zipFactory = zipFactory;
         m_refDir = new File(location.substring(
             location.indexOf(BundleArchive.FILE_PROTOCOL)
                 + BundleArchive.FILE_PROTOCOL.length()));
@@ -95,7 +99,8 @@
 
     public synchronized Content getContent() throws Exception
     {
-        return new DirectoryContent(getLogger(), getConfig(), this, getRevisionRootDir(), m_refDir);
+        return new DirectoryContent(getLogger(), getConfig(), m_zipFactory,
+            this, getRevisionRootDir(), m_refDir);
     }
 
     protected void close() throws Exception
diff --git a/framework/src/main/java/org/apache/felix/framework/cache/JarContent.java b/framework/src/main/java/org/apache/felix/framework/cache/JarContent.java
index f8d0bd5..409f12e 100644
--- a/framework/src/main/java/org/apache/felix/framework/cache/JarContent.java
+++ b/framework/src/main/java/org/apache/felix/framework/cache/JarContent.java
@@ -32,8 +32,9 @@
 import java.util.zip.ZipEntry;
 import org.apache.felix.framework.Logger;
 import org.apache.felix.framework.util.FelixConstants;
-import org.apache.felix.framework.util.ZipFileX;
 import org.apache.felix.framework.util.Util;
+import org.apache.felix.framework.util.WeakZipFileFactory;
+import org.apache.felix.framework.util.WeakZipFileFactory.WeakZipFile;
 import org.osgi.framework.Constants;
 
 public class JarContent implements Content
@@ -44,22 +45,39 @@
 
     private final Logger m_logger;
     private final Map m_configMap;
+    private final WeakZipFileFactory m_zipFactory;
     private final Object m_revisionLock;
     private final File m_rootDir;
     private final File m_file;
-    private final ZipFileX m_zipFile;
+    private final WeakZipFile m_zipFile;
     private final boolean m_isZipFileOwner;
     private Map m_nativeLibMap;
 
-    public JarContent(Logger logger, Map configMap, Object revisionLock, File rootDir,
-        File file, ZipFileX zipFile)
+    public JarContent(Logger logger, Map configMap, WeakZipFileFactory zipFactory,
+        Object revisionLock, File rootDir, File file, WeakZipFile zipFile)
     {
         m_logger = logger;
         m_configMap = configMap;
+        m_zipFactory = zipFactory;
         m_revisionLock = revisionLock;
         m_rootDir = rootDir;
         m_file = file;
-        m_zipFile = (zipFile == null) ? openZipFile(m_file) : zipFile;
+        if (zipFile == null)
+        {
+            try
+            {
+                m_zipFile = m_zipFactory.create(m_file);
+            }
+            catch (IOException ex)
+            {
+                throw new RuntimeException(
+                    "Unable to open JAR file, probably deleted: " + ex.getMessage());
+            }
+        }
+        else
+        {
+            m_zipFile = zipFile;
+        }
         m_isZipFileOwner = (zipFile == null);
     }
 
@@ -209,7 +227,7 @@
         // just return it immediately.
         if (entryName.equals(FelixConstants.CLASS_PATH_DOT))
         {
-            return new JarContent(m_logger, m_configMap, m_revisionLock,
+            return new JarContent(m_logger, m_configMap, m_zipFactory, m_revisionLock,
                 m_rootDir, m_file, m_zipFile);
         }
 
@@ -272,7 +290,7 @@
                 }
             }
             return new JarContent(
-                m_logger, m_configMap, m_revisionLock,
+                m_logger, m_configMap, m_zipFactory, m_revisionLock,
                 extractJar.getParentFile(), extractJar, null);
         }
 
@@ -477,19 +495,6 @@
         }
     }
 
-    private static ZipFileX openZipFile(File file) throws RuntimeException
-    {
-        try
-        {
-            return BundleCache.getSecureAction().openZipFile(file);
-        }
-        catch (IOException ex)
-        {
-            throw new RuntimeException(
-                "Unable to open JAR file, probably deleted: " + ex.getMessage());
-        }
-    }
-
     private static class EntriesEnumeration implements Enumeration<String>
     {
         private final Enumeration m_enumeration;
diff --git a/framework/src/main/java/org/apache/felix/framework/cache/JarRevision.java b/framework/src/main/java/org/apache/felix/framework/cache/JarRevision.java
index b953767..7a539da 100644
--- a/framework/src/main/java/org/apache/felix/framework/cache/JarRevision.java
+++ b/framework/src/main/java/org/apache/felix/framework/cache/JarRevision.java
@@ -29,9 +29,10 @@
 import java.util.zip.ZipEntry;
 
 import org.apache.felix.framework.Logger;
-import org.apache.felix.framework.util.ZipFileX;
 import org.apache.felix.framework.util.StringMap;
 import org.apache.felix.framework.util.Util;
+import org.apache.felix.framework.util.WeakZipFileFactory;
+import org.apache.felix.framework.util.WeakZipFileFactory.WeakZipFile;
 
 /**
  * <p>
@@ -48,24 +49,19 @@
 {
     private static final transient String BUNDLE_JAR_FILE = "bundle.jar";
 
-    private File m_bundleFile = null;
-    private final ZipFileX m_zipFile;
+    private final WeakZipFileFactory m_zipFactory;
+    private final File m_bundleFile;
+    private final WeakZipFile m_zipFile;
 
     public JarRevision(
-        Logger logger, Map configMap, File revisionRootDir,
-        String location, boolean byReference)
-        throws Exception
-    {
-        this(logger, configMap, revisionRootDir, location, byReference, null);
-    }
-
-    public JarRevision(
-        Logger logger, Map configMap, File revisionRootDir, String location,
-        boolean byReference, InputStream is)
+        Logger logger, Map configMap, WeakZipFileFactory zipFactory,
+        File revisionRootDir, String location, boolean byReference, InputStream is)
         throws Exception
     {
         super(logger, configMap, revisionRootDir, location);
 
+        m_zipFactory = zipFactory;
+
         if (byReference)
         {
             m_bundleFile = new File(location.substring(
@@ -81,11 +77,11 @@
         initialize(byReference, is);
 
         // Open shared copy of the JAR file.
-        ZipFileX zipFile = null;
+        WeakZipFile zipFile = null;
         try
         {
             // Open bundle JAR file.
-            zipFile = BundleCache.getSecureAction().openZipFile(m_bundleFile);
+            zipFile = m_zipFactory.create(m_bundleFile);
             // Error if no jar file.
             if (zipFile == null)
             {
@@ -111,8 +107,8 @@
 
     public synchronized Content getContent() throws Exception
     {
-        return new JarContent(getLogger(), getConfig(), this, getRevisionRootDir(),
-            m_bundleFile, m_zipFile);
+        return new JarContent(getLogger(), getConfig(), m_zipFactory,
+            this, getRevisionRootDir(), m_bundleFile, m_zipFile);
     }
 
     protected void close() throws Exception
@@ -198,7 +194,7 @@
     // The idea is to not open the jar file as a java.util.jarfile but
     // read the mainfest from the zipfile directly and parse it manually
     // to use less memory and be faster.
-    private static void getMainAttributes(Map result, ZipFileX zipFile) throws Exception
+    private static void getMainAttributes(Map result, WeakZipFile zipFile) throws Exception
     {
         ZipEntry entry = zipFile.getEntry("META-INF/MANIFEST.MF");
 
diff --git a/framework/src/main/java/org/apache/felix/framework/util/Mutex.java b/framework/src/main/java/org/apache/felix/framework/util/Mutex.java
new file mode 100644
index 0000000..fdec057
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/util/Mutex.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.framework.util;
+
+public class Mutex
+{
+    private int m_count;
+
+    public Mutex()
+    {
+        m_count = 1;
+    }
+
+    public synchronized void down() throws InterruptedException
+    {
+        while (m_count <= 0)
+        {
+            wait();
+        }
+        m_count = 0;
+    }
+
+    public synchronized void up()
+    {
+        m_count = 1;
+        notifyAll();
+    }
+}
\ No newline at end of file
diff --git a/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java b/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java
index ead1ad9..50d3a13 100644
--- a/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java
+++ b/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java
@@ -26,6 +26,7 @@
 import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.Map;
+import java.util.zip.ZipFile;
 import org.osgi.framework.Bundle;
 
 import org.osgi.framework.BundleActivator;
@@ -594,7 +595,7 @@
         }
     }
 
-    public ZipFileX openZipFile(File file) throws IOException
+    public ZipFile openZipFile(File file) throws IOException
     {
         if (System.getSecurityManager() != null)
         {
@@ -602,7 +603,7 @@
             {
                 Actions actions = (Actions) m_actions.get();
                 actions.set(Actions.OPEN_ZIPFILE_ACTION, file);
-                return (ZipFileX) AccessController.doPrivileged(actions, m_acc);
+                return (ZipFile) AccessController.doPrivileged(actions, m_acc);
             }
             catch (PrivilegedActionException ex)
             {
@@ -615,7 +616,7 @@
         }
         else
         {
-            return new ZipFileX(file);
+            return new ZipFile(file);
         }
     }
 
@@ -1587,7 +1588,7 @@
                 case MAKE_DIRECTORY_ACTION:
                     return ((File) arg1).mkdir() ? Boolean.TRUE : Boolean.FALSE;
                 case OPEN_ZIPFILE_ACTION:
-                    return new ZipFileX((File) arg1);
+                    return new ZipFile((File) arg1);
                 case OPEN_URLCONNECTION_ACTION:
                     return ((URL) arg1).openConnection();
                 case RENAME_FILE_ACTION:
diff --git a/framework/src/main/java/org/apache/felix/framework/util/WeakZipFileFactory.java b/framework/src/main/java/org/apache/felix/framework/util/WeakZipFileFactory.java
new file mode 100644
index 0000000..b8dc046
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/util/WeakZipFileFactory.java
@@ -0,0 +1,746 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.framework.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * This class implements a factory for creating weak zip files, which behave
+ * mostly like a ZipFile, but can be weakly closed to limit the number of
+ * open files.
+ */
+public class WeakZipFileFactory
+{
+    private static final int WEAKLY_CLOSED = 0;
+    private static final int OPEN = 1;
+    private static final int CLOSED = 2;
+
+    private static final SecureAction m_secureAction = new SecureAction();
+
+    private final List<WeakZipFile> m_zipFiles = new ArrayList<WeakZipFile>();
+    private final List<WeakZipFile> m_openFiles = new ArrayList<WeakZipFile>();
+    private final Mutex m_globalMutex = new Mutex();
+    private final int m_limit;
+
+    /**
+     * Constructs a weak zip file factory with the specified file limit. A limit
+     * of zero signified no limit.
+     * @param limit maximum number of open zip files at any given time.
+     */
+    public WeakZipFileFactory(int limit)
+    {
+        if (limit < 0)
+        {
+            throw new IllegalArgumentException("Limit must be non-negative.");
+        }
+        m_limit = limit;
+    }
+
+    /**
+     * Factory method used to create weak zip file.
+     * @param file the target zip file.
+     * @return the created weak zip file.
+     * @throws IOException if the zip file could not be opened.
+     */
+    public WeakZipFile create(File file) throws IOException
+    {
+        WeakZipFile wzf = new WeakZipFile(file);
+
+        if (m_limit > 0)
+        {
+            try
+            {
+                m_globalMutex.down();
+            }
+            catch (InterruptedException ex)
+            {
+                Thread.currentThread().interrupt();
+                throw new IOException("Interrupted while acquiring global zip file mutex.");
+            }
+
+            try
+            {
+                m_zipFiles.add(wzf);
+                m_openFiles.add(wzf);
+                if (m_openFiles.size() > m_limit)
+                {
+                    WeakZipFile candidate = m_openFiles.get(0);
+                    for (WeakZipFile tmp : m_openFiles)
+                    {
+                        if (candidate.m_timestamp > tmp.m_timestamp)
+                        {
+                            candidate = tmp;
+                        }
+                    }
+                    candidate._closeWeakly();
+                }
+            }
+            finally
+            {
+                m_globalMutex.up();
+            }
+        }
+
+        return wzf;
+    }
+
+    /**
+     * Only used for testing.
+     * @return unclosed weak zip files.
+     **/
+    List<WeakZipFile> getZipZiles()
+    {
+        try
+        {
+            m_globalMutex.down();
+        }
+        catch (InterruptedException ex)
+        {
+            Thread.currentThread().interrupt();
+            return Collections.EMPTY_LIST;
+        }
+
+        try
+        {
+            return m_zipFiles;
+        }
+        finally
+        {
+            m_globalMutex.up();
+        }
+    }
+
+    /**
+     * Only used for testing.
+     * @return open weak zip files.
+     **/
+    List<WeakZipFile> getOpenZipZiles()
+    {
+        try
+        {
+            m_globalMutex.down();
+        }
+        catch (InterruptedException ex)
+        {
+            Thread.currentThread().interrupt();
+            return Collections.EMPTY_LIST;
+        }
+
+        try
+        {
+            return m_openFiles;
+        }
+        finally
+        {
+            m_globalMutex.up();
+        }
+    }
+
+    /**
+     * This class wraps a ZipFile to making it possible to weakly close it;
+     * this means the underlying zip file will be automatically reopened on demand
+     * if anyone tries to use it.
+     */
+    public class WeakZipFile
+    {
+        private final File m_file;
+        private final Mutex m_localMutex = new Mutex();
+        private ZipFile m_zipFile;
+        private int m_status = OPEN;
+        private long m_timestamp;
+
+        /**
+         * Constructor is private since instances need to be centrally
+         * managed.
+         * @param file the target zip file.
+         * @throws IOException if the zip file could not be opened.
+         */
+        private WeakZipFile(File file) throws IOException
+        {
+            m_file = file;
+            m_zipFile = m_secureAction.openZipFile(m_file);
+            m_timestamp = System.currentTimeMillis();
+        }
+
+        /**
+         * Returns the specified entry from the zip file.
+         * @param name the name of the entry to return.
+         * @return the zip entry associated with the specified name or null
+         *         if it does not exist.
+         */
+        public ZipEntry getEntry(String name)
+        {
+            ensureZipFileIsOpen();
+
+            try
+            {
+                ZipEntry ze = null;
+                ze = m_zipFile.getEntry(name);
+                if ((ze != null) && (ze.getSize() == 0) && !ze.isDirectory())
+                {
+                    //The attempts to fix an apparent bug in the JVM in versions
+                    // 1.4.2 and lower where directory entries in ZIP/JAR files are
+                    // not correctly identified.
+                    ZipEntry dirEntry = m_zipFile.getEntry(name + '/');
+                    if (dirEntry != null)
+                    {
+                        ze = dirEntry;
+                    }
+                }
+                return ze;
+            }
+            finally
+            {
+                m_localMutex.up();
+            }
+        }
+
+        /**
+         * Returns an enumeration of zip entries from the zip file.
+         * @return an enumeration of zip entries.
+         */
+        public Enumeration<ZipEntry> entries()
+        {
+            ensureZipFileIsOpen();
+
+            try
+            {
+                // We need to suck in all of the entries since the zip
+                // file may get weakly closed during iteration. Technically,
+                // this may not be 100% correct either since if the zip file
+                // gets weakly closed and reopened, then the zip entries
+                // will be from a different zip file. It is not clear if this
+                // will cause any issues.
+                Enumeration<? extends ZipEntry> e = m_zipFile.entries();
+                List<ZipEntry> entries = new ArrayList<ZipEntry>();
+                while (e.hasMoreElements())
+                {
+                    entries.add(e.nextElement());
+                }
+                return Collections.enumeration(entries);
+            }
+            finally
+            {
+                m_localMutex.up();
+            }
+        }
+
+        /**
+         * Returns an input stream for the specified zip entry.
+         * @param ze the zip entry whose input stream is to be retrieved.
+         * @return an input stream to the zip entry.
+         * @throws IOException if the input stream cannot be opened.
+         */
+        public InputStream getInputStream(ZipEntry ze) throws IOException
+        {
+            ensureZipFileIsOpen();
+
+            try
+            {
+                InputStream is = m_zipFile.getInputStream(ze);
+                return new WeakZipInputStream(ze.getName(), is);
+            }
+            finally
+            {
+                m_localMutex.up();
+            }
+        }
+
+        /**
+         * Weakly closes the zip file, which means that it will be reopened
+         * if anyone tries to use it again.
+         */
+        void closeWeakly()
+        {
+            try
+            {
+                m_globalMutex.down();
+            }
+            catch (InterruptedException ex)
+            {
+                Thread.currentThread().interrupt();
+                throw new IllegalStateException(
+                    "Interrupted while acquiring global zip file mutex.");
+            }
+
+            try
+            {
+                _closeWeakly();
+            }
+            finally
+            {
+                m_globalMutex.up();
+            }
+        }
+
+        /**
+         * This method is used internally to weakly close a zip file. It should
+         * only be called when already holding the global lock, otherwise use
+         * closeWeakly().
+         */
+        private void _closeWeakly()
+        {
+            try
+            {
+                m_localMutex.down();
+            }
+            catch (InterruptedException ex)
+            {
+                Thread.currentThread().interrupt();
+                throw new IllegalStateException(
+                    "Interrupted while acquiring local zip file mutex.");
+            }
+
+            try
+            {
+                if (m_status == OPEN)
+                {
+                    try
+                    {
+                        m_status = WEAKLY_CLOSED;
+                        if (m_zipFile != null)
+                        {
+                            m_zipFile.close();
+                            m_zipFile = null;
+                        }
+                        m_openFiles.remove(this);
+                    }
+                    catch (IOException ex)
+                    {
+                        __close();
+                    }
+                }
+            }
+            finally
+            {
+                m_localMutex.up();
+            }
+        }
+
+        /**
+         * This method permanently closes the zip file.
+         * @throws IOException if any error occurs while trying to close the
+         *         zip file.
+         */
+        public void close() throws IOException
+        {
+            if (m_limit > 0)
+            {
+                try
+                {
+                    m_globalMutex.down();
+                }
+                catch (InterruptedException ex)
+                {
+                    Thread.currentThread().interrupt();
+                    throw new IllegalStateException(
+                        "Interrupted while acquiring global zip file mutex.");
+                }
+                try
+                {
+                    m_localMutex.down();
+                }
+                catch (InterruptedException ex)
+                {
+                    m_globalMutex.up();
+                    Thread.currentThread().interrupt();
+                    throw new IllegalStateException(
+                        "Interrupted while acquiring local zip file mutex.");
+                }
+            }
+
+            try
+            {
+                ZipFile tmp = m_zipFile;
+                __close();
+                if (tmp != null)
+                {
+                    tmp.close();
+                }
+            }
+            finally
+            {
+                m_localMutex.up();
+                m_globalMutex.up();
+            }
+        }
+
+        /**
+         * This internal method is used to clear the zip file from the data
+         * structures and reset its state. It should only be called when
+         * holding the global and local mutexes.
+         */
+        private void __close()
+        {
+            m_status = CLOSED;
+            m_zipFile = null;
+            m_zipFiles.remove(this);
+            m_openFiles.remove(this);
+        }
+
+        /**
+         * This method ensures that the zip file associated with this
+         * weak zip file instance is actually open and acquires the
+         * local weak zip file mutex. If the underlying zip file is closed,
+         * then the local mutex is released and an IllegalStateException is
+         * thrown. If the zip file is weakly closed, then it is reopened.
+         * If the zip file is already opened, then no additional action is
+         * necessary. If this method does not throw an exception, then
+         * the end result is the zip file member field is non-null and the
+         * local mutex has been acquired.
+         */
+        private void ensureZipFileIsOpen()
+        {
+            if (m_limit == 0)
+            {
+                return;
+            }
+
+            // Get mutex for zip file.
+            try
+            {
+                m_localMutex.down();
+            }
+            catch (InterruptedException ex)
+            {
+                Thread.currentThread().interrupt();
+                throw new IllegalStateException(
+                    "Interrupted while acquiring local zip file mutex.");
+            }
+
+            // If zip file is closed, then just return null.
+            if (m_status == CLOSED)
+            {
+                m_localMutex.up();
+                throw new IllegalStateException("Zip file is closed: " + m_file);
+            }
+
+            // If zip file is weakly closed, we need to reopen it,
+            // but we have to release the zip mutex to acquire the
+            // global mutex, then reacquire the zip mutex. This
+            // ensures that the global mutex is always acquired
+            // before any local mutex to avoid deadlocks.
+            IOException cause = null;
+            if (m_status == WEAKLY_CLOSED)
+            {
+                m_localMutex.up();
+
+                try
+                {
+                    m_globalMutex.down();
+                }
+                catch (InterruptedException ex)
+                {
+                    Thread.currentThread().interrupt();
+                    throw new IllegalStateException(
+                        "Interrupted while acquiring global zip file mutex.");
+                }
+                try
+                {
+                    m_localMutex.down();
+                }
+                catch (InterruptedException ex)
+                {
+                    m_globalMutex.up();
+                    Thread.currentThread().interrupt();
+                    throw new IllegalStateException(
+                        "Interrupted while acquiring local zip file mutex.");
+                }
+
+                // Double check status since it may have changed.
+                if (m_status == CLOSED)
+                {
+                    m_localMutex.up();
+                    m_globalMutex.up();
+                    throw new IllegalStateException("Zip file is closed: " + m_file);
+                }
+                else if (m_status == WEAKLY_CLOSED)
+                {
+                    try
+                    {
+                        __reopenZipFile();
+                    }
+                    catch (IOException ex)
+                    {
+                        cause = ex;
+                    }
+                }
+
+                // Release the global mutex, since it should no longer be necessary.
+                m_globalMutex.up();
+            }
+
+            // It is possible that reopening the zip file failed, so we check
+            // for that case and throw an exception.
+            if (m_zipFile == null)
+            {
+                m_localMutex.up();
+                IllegalStateException ise =
+                    new IllegalStateException("Zip file is closed: " + m_file);
+                if (cause != null)
+                {
+                    ise.initCause(cause);
+                }
+                throw ise;
+            }
+        }
+
+        /**
+         * Thie internal method is used to reopen a weakly closed zip file.
+         * It makes a best effort, but may fail and leave the zip file member
+         * field null. Any failure reopening a zip file results in it being
+         * permanently closed. This method should only be invoked when holding
+         * the global and local mutexes.
+         */
+        private void __reopenZipFile() throws IOException
+        {
+            if (m_status == WEAKLY_CLOSED)
+            {
+                try
+                {
+                    m_zipFile = m_secureAction.openZipFile(m_file);
+                    m_status = OPEN;
+                    m_timestamp = System.currentTimeMillis();
+                }
+                catch (IOException ex)
+                {
+                    __close();
+                    throw ex;
+                }
+
+                if (m_zipFile != null)
+                {
+                    m_openFiles.add(this);
+                    if (m_openFiles.size() > m_limit)
+                    {
+                        WeakZipFile candidate = m_openFiles.get(0);
+                        for (WeakZipFile tmp : m_openFiles)
+                        {
+                            if (candidate.m_timestamp > tmp.m_timestamp)
+                            {
+                                candidate = tmp;
+                            }
+                        }
+                        candidate._closeWeakly();
+                    }
+                }
+            }
+        }
+
+        /**
+         * This is an InputStream wrapper that will properly reopen the underlying
+         * zip file if it is weakly closed and create the underlying input stream.
+         */
+        class WeakZipInputStream extends InputStream
+        {
+            private final String m_entryName;
+            private InputStream m_is;
+            private int m_currentPos = 0;
+            private ZipFile m_zipFileSnapshot;
+
+            WeakZipInputStream(String entryName, InputStream is)
+            {
+                m_entryName = entryName;
+                m_is = is;
+                m_zipFileSnapshot = m_zipFile;
+            }
+
+            /**
+             * This internal method ensures that the zip file is open and that
+             * the underlying input stream is valid. Upon successful completion,
+             * the underlying input stream will be valid and the local mutex
+             * will be held.
+             * @throws IOException if the was an error handling the input stream.
+             */
+            private void ensureInputStreamIsValid() throws IOException
+            {
+                if (m_limit == 0)
+                {
+                    return;
+                }
+
+                ensureZipFileIsOpen();
+
+                // If the underlying zip file changed, then we need
+                // to get the input stream again.
+                if (m_zipFileSnapshot != m_zipFile)
+                {
+                    m_zipFileSnapshot = m_zipFile;
+
+                    if (m_is != null)
+                    {
+                        try
+                        {
+                            m_is.close();
+                        }
+                        catch (Exception ex)
+                        {
+                            // Not much we can do.
+                        }
+                    }
+                    try
+                    {
+                        m_is = m_zipFile.getInputStream(m_zipFile.getEntry(m_entryName));
+                        m_is.skip(m_currentPos);
+                    }
+                    catch (IOException ex)
+                    {
+                        m_localMutex.up();
+                        throw ex;
+                    }
+                }
+            }
+
+            @Override
+            public int available() throws IOException
+            {
+                ensureInputStreamIsValid();
+                try
+                {
+                    return m_is.available();
+                }
+                finally
+                {
+                    m_localMutex.up();
+                }
+            }
+
+            @Override
+            public void close() throws IOException
+            {
+                ensureInputStreamIsValid();
+                try
+                {
+                    InputStream is = m_is;
+                    m_is = null;
+                    if (is != null)
+                    {
+                        is.close();
+                    }
+                }
+                finally
+                {
+                    m_localMutex.up();
+                }
+            }
+
+            @Override
+            public void mark(int i)
+            {
+                // Not supported.
+            }
+
+            @Override
+            public boolean markSupported()
+            {
+                // Not supported.
+                return false;
+            }
+
+            public int read() throws IOException
+            {
+                ensureInputStreamIsValid();
+                try
+                {
+                    int len = m_is.read();
+                    if (len > 0)
+                    {
+                        m_currentPos++;
+                    }
+                    return len;
+                }
+                finally
+                {
+                    m_localMutex.up();
+                }
+            }
+
+            @Override
+            public int read(byte[] bytes) throws IOException
+            {
+                ensureInputStreamIsValid();
+                try
+                {
+                    int len = m_is.read(bytes);
+                    if (len > 0)
+                    {
+                        m_currentPos += len;
+                    }
+                    return len;
+                }
+                finally
+                {
+                    m_localMutex.up();
+                }
+            }
+
+            @Override
+            public int read(byte[] bytes, int i, int i1) throws IOException
+            {
+                ensureInputStreamIsValid();
+                try
+                {
+                    int len = m_is.read(bytes, i, i1);
+                    if (len > 0)
+                    {
+                        m_currentPos += len;
+                    }
+                    return len;
+                }
+                finally
+                {
+                    m_localMutex.up();
+                }
+            }
+
+            @Override
+            public void reset() throws IOException
+            {
+                throw new IOException("Unsupported operation");
+            }
+
+            @Override
+            public long skip(long l) throws IOException
+            {
+                ensureInputStreamIsValid();
+                try
+                {
+                    long len = m_is.skip(l);
+                    if (len > 0)
+                    {
+                        m_currentPos += len;
+                    }
+                    return len;
+                }
+                finally
+                {
+                    m_localMutex.up();
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/framework/src/main/java/org/apache/felix/framework/util/ZipFileX.java b/framework/src/main/java/org/apache/felix/framework/util/ZipFileX.java
deleted file mode 100644
index 550a680..0000000
--- a/framework/src/main/java/org/apache/felix/framework/util/ZipFileX.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/* 
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.felix.framework.util;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.jar.JarEntry;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
-
-/**
- * The purpose of this class is to fix an apparent bug in the JVM in versions
- * 1.4.2 and lower where directory entries in ZIP/JAR files are not correctly
- * identified.
-**/
-public class ZipFileX extends ZipFile
-{
-    public ZipFileX(File file) throws IOException
-    {
-        super(file);
-    }
-
-
-    public ZipFileX(File file, int mode) throws IOException
-    {
-        super(file, mode);
-    }
-
-    public ZipFileX(String name) throws IOException
-    {
-        super(name);
-    }
-
-    public ZipEntry getEntry(String name)
-    {
-        ZipEntry entry = super.getEntry(name);
-        if ((entry != null) && (entry.getSize() == 0) && !entry.isDirectory())
-        {
-            ZipEntry dirEntry = super.getEntry(name + '/');
-            if (dirEntry != null)
-            {
-                entry = dirEntry;
-            }
-        }
-        return entry;
-    }
-}
\ No newline at end of file
diff --git a/framework/src/test/java/org/apache/felix/framework/util/WeakZipFileTest.java b/framework/src/test/java/org/apache/felix/framework/util/WeakZipFileTest.java
new file mode 100644
index 0000000..1847056
--- /dev/null
+++ b/framework/src/test/java/org/apache/felix/framework/util/WeakZipFileTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.framework.util;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import junit.framework.TestCase;
+import org.apache.felix.framework.util.WeakZipFileFactory.WeakZipFile;
+
+public class WeakZipFileTest extends TestCase
+{
+    private static final String ENTRY_NAME = "entry.txt";
+
+    public void testWeakClose()
+    {
+        // Create a reasonably big random string.
+        byte[] contentBytes = new byte[16384];
+        for (int i = 0; i < contentBytes.length; i++)
+        {
+            contentBytes[i] = (byte) ((i % 65) + 65);
+        }
+        String contentString = new String(contentBytes);
+
+        // Create a temporary zip file.
+        File tmpZip = null;
+        try
+        {
+            tmpZip = File.createTempFile("felix.test", ".zip");
+            tmpZip.deleteOnExit();
+            ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(tmpZip));
+            ZipEntry ze = new ZipEntry(ENTRY_NAME);
+            zos.putNextEntry(ze);
+            zos.write(contentBytes, 0, contentBytes.length);
+            zos.close();
+        }
+        catch (IOException ex)
+        {
+            fail("Unable to create temporary zip file: " + ex);
+        }
+
+        try
+        {
+            WeakZipFileFactory factory = new WeakZipFileFactory(1);
+            WeakZipFile zipFile = factory.create(tmpZip);
+            assertTrue("Zip file not recorded.",
+                factory.getZipZiles().contains(zipFile));
+            assertTrue("Open zip file not recorded.",
+                factory.getOpenZipZiles().contains(zipFile));
+            ZipEntry ze = zipFile.getEntry(ENTRY_NAME);
+            assertNotNull("Zip entry not found", ze);
+            byte[] firstHalf = new byte[contentBytes.length / 2];
+            byte[] secondHalf = new byte[contentBytes.length - firstHalf.length];
+            InputStream is = zipFile.getInputStream(ze);
+            is.read(firstHalf);
+            zipFile.closeWeakly();
+            assertTrue("Zip file not recorded.",
+                factory.getZipZiles().contains(zipFile));
+            assertFalse("Open zip file still recorded.",
+                factory.getOpenZipZiles().contains(zipFile));
+            is.read(secondHalf);
+            assertTrue("Zip file not recorded.",
+                factory.getZipZiles().contains(zipFile));
+            assertTrue("Open zip file not recorded.",
+                factory.getOpenZipZiles().contains(zipFile));
+            byte[] complete = new byte[firstHalf.length + secondHalf.length];
+            System.arraycopy(firstHalf, 0, complete, 0, firstHalf.length);
+            System.arraycopy(secondHalf, 0, complete, firstHalf.length, secondHalf.length);
+            String completeString = new String(complete);
+            assertEquals(contentString, completeString);
+            zipFile.close();
+        }
+        catch (IOException ex)
+        {
+            fail("Unable to read zip file entry: " + ex);
+        }
+    }
+}
\ No newline at end of file