blob: 3e13c3e7e78accb248e45f902c43dde2c1534eb4 [file] [log] [blame]
/*
* 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.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
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 Lock m_globalMutex = new ReentrantLock();
private final int m_limit;
/**
* Constructs a weak zip file factory with the specified file limit. A limit
* of zero signifies 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 files.
* @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)
{
m_globalMutex.lock();
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.unlock();
}
}
return wzf;
}
/**
* Only used for testing.
* @return unclosed weak zip files.
**/
List<WeakZipFile> getZipZiles()
{
m_globalMutex.lock();
try
{
return m_zipFiles;
}
finally
{
m_globalMutex.unlock();
}
}
/**
* Only used for testing.
* @return open weak zip files.
**/
List<WeakZipFile> getOpenZipZiles()
{
m_globalMutex.lock();
try
{
return m_openFiles;
}
finally
{
m_globalMutex.unlock();
}
}
/**
* 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 Lock m_localMutex = new ReentrantLock(false);
private volatile ZipFile m_zipFile;
private volatile int m_status = OPEN;
private volatile long m_timestamp;
private volatile SoftReference<LinkedHashMap<String, ZipEntry>> m_entries;
/**
* 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
{
LinkedHashMap<String, ZipEntry> entries = getEntries(false);
ZipEntry ze;
if (entries != null)
{
ze = entries.get(name);
if (ze == null)
{
ze = entries.get(name + "/");
}
}
else
{
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
{
if (m_limit > 0)
{
m_localMutex.unlock();
}
}
}
/**
* Returns an enumeration of zip entries from the zip file.
* @return an enumeration of zip entries.
*/
public Enumeration<ZipEntry> entries()
{
ensureZipFileIsOpen();
try
{
LinkedHashMap<String, ZipEntry> entries = getEntries(true);
return Collections.enumeration(entries.values());
}
finally
{
if (m_limit > 0)
{
m_localMutex.unlock();
}
}
}
public Enumeration<String> names()
{
ensureZipFileIsOpen();
try
{
LinkedHashMap<String, ZipEntry> entries = getEntries(true);
return Collections.enumeration(entries.keySet());
}
finally
{
if (m_limit > 0)
{
m_localMutex.unlock();
}
}
}
private LinkedHashMap<String, ZipEntry> getEntries(boolean create)
{
LinkedHashMap<String, ZipEntry> entries = null;
if (m_entries != null)
{
entries = m_entries.get();
}
if (entries == null && create)
{
synchronized (m_zipFile)
{
if (m_entries != null)
{
entries = m_entries.get();
}
if (entries == null)
{
// 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();
entries = new LinkedHashMap<String, ZipEntry>();
while (e.hasMoreElements())
{
ZipEntry entry = e.nextElement();
entries.put(entry.getName(), entry);
}
m_entries = new SoftReference<LinkedHashMap<String, ZipEntry>>(entries);
}
}
}
return entries;
}
/**
* 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 m_limit == 0 ? is : new WeakZipInputStream(ze.getName(), is);
}
finally
{
if (m_limit > 0)
{
m_localMutex.unlock();
}
}
}
/**
* Weakly closes the zip file, which means that it will be reopened
* if anyone tries to use it again.
*/
void closeWeakly()
{
m_globalMutex.lock();
try
{
_closeWeakly();
}
finally
{
m_globalMutex.unlock();
}
}
/**
* 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()
{
m_localMutex.lock();
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.unlock();
}
}
/**
* 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)
{
m_globalMutex.lock();
m_localMutex.lock();
}
try
{
ZipFile tmp = m_zipFile;
__close();
if (tmp != null)
{
tmp.close();
}
}
finally
{
if (m_limit > 0)
{
m_localMutex.unlock();
m_globalMutex.unlock();
}
}
}
/**
* 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.
m_localMutex.lock();
// If zip file is closed, then just return null.
if (m_status == CLOSED)
{
m_localMutex.unlock();
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.unlock();
m_globalMutex.lock();
m_localMutex.lock();
// Double check status since it may have changed.
if (m_status == CLOSED)
{
m_localMutex.unlock();
m_globalMutex.unlock();
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.unlock();
}
// 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.unlock();
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 volatile InputStream m_is;
private volatile int m_currentPos = 0;
private volatile 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)
{
if (m_limit > 0)
{
m_localMutex.unlock();
}
throw ex;
}
}
}
@Override
public int available() throws IOException
{
ensureInputStreamIsValid();
try
{
return m_is.available();
}
finally
{
if (m_limit > 0)
{
m_localMutex.unlock();
}
}
}
@Override
public void close() throws IOException
{
ensureInputStreamIsValid();
try
{
InputStream is = m_is;
m_is = null;
if (is != null)
{
is.close();
}
}
finally
{
if (m_limit > 0)
{
m_localMutex.unlock();
}
}
}
public int read() throws IOException
{
ensureInputStreamIsValid();
try
{
int len = m_is.read();
if (len > 0)
{
m_currentPos++;
}
return len;
}
finally
{
if (m_limit > 0)
{
m_localMutex.unlock();
}
}
}
@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
{
if (m_limit > 0)
{
m_localMutex.unlock();
}
}
}
@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
{
if (m_limit > 0)
{
m_localMutex.unlock();
}
}
}
@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
{
if (m_limit > 0)
{
m_localMutex.unlock();
}
}
}
}
}
}