blob: db62f0741a5f8c2546ca4841088f6c2ffcf849fe [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2018 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Rob Harrop - SpringSource Inc. (bug 253942)
*******************************************************************************/
package org.eclipse.osgi.storage.bundlefile;
import java.io.File;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.eclipse.osgi.container.ModuleContainerAdaptor.ContainerEvent;
import org.eclipse.osgi.container.ModuleRevision;
import org.eclipse.osgi.framework.log.FrameworkLogEntry;
import org.eclipse.osgi.internal.debug.Debug;
import org.eclipse.osgi.internal.framework.EquinoxContainer;
import org.eclipse.osgi.internal.messages.Msg;
import org.eclipse.osgi.storage.BundleInfo;
import org.eclipse.osgi.storage.Storage.StorageException;
import org.eclipse.osgi.util.NLS;
/**
* A BundleFile that uses a ZipFile as it base file.
*/
public class ZipBundleFile extends BundleFile {
// A reentrant lock is used here (instead of intrinsic synchronization)
// to allow the lock conditional held
// see lockOpen() and getZipFile()
private final ReentrantLock openLock = new ReentrantLock();
private final Condition refCondition = openLock.newCondition();
private final MRUBundleFileList mruList;
private final BundleInfo.Generation generation;
private final Debug debug;
/**
* The zip file
*/
private volatile ZipFile zipFile;
/**
* The closed flag
*/
private volatile boolean closed = true;
private int referenceCount = 0;
public ZipBundleFile(File basefile, BundleInfo.Generation generation, MRUBundleFileList mruList, Debug debug) throws IOException {
super(basefile);
if (!BundleFile.secureAction.exists(basefile))
throw new IOException(NLS.bind(Msg.ADAPTER_FILEEXIST_EXCEPTION, basefile));
this.debug = debug;
this.generation = generation;
this.closed = true;
this.mruList = mruList;
}
/**
* Checks if the zip file is open
* @return true if the zip file is open
*/
private boolean lockOpen() {
try {
return getZipFile(true) != null;
} catch (IOException e) {
if (generation != null) {
ModuleRevision r = generation.getRevision();
if (r != null) {
ContainerEvent eventType = ContainerEvent.ERROR;
// If the revision has been removed from the list of revisions then it has been deleted
// because the bundle has been uninstalled or updated
if (!r.getRevisions().getModuleRevisions().contains(r)) {
// instead of filling the log with errors about missing files from
// uninstalled/updated bundles just give it an info level
eventType = ContainerEvent.INFO;
}
generation.getBundleInfo().getStorage().getAdaptor().publishContainerEvent(eventType, r.getRevisions().getModule(), e);
}
}
// TODO not sure if throwing a runtime exception is better
// throw new RuntimeException("Failed to open bundle file.", e);
return false;
}
}
/**
* Returns an open ZipFile for this bundle file. If an open
* ZipFile does not exist then a new one is created and
* returned.
* @param keepLock true if the open zip lock should be retained
* @return an open ZipFile for this bundle
* @throws IOException
*/
private ZipFile getZipFile(boolean keepLock) throws IOException {
openLock.lock();
try {
if (closed) {
boolean needBackPressure = mruListAdd();
if (needBackPressure) {
// release lock before applying back pressure
openLock.unlock();
try {
mruListApplyBackPressure();
} finally {
// get lock back after back pressure
openLock.lock();
}
}
// check close again after getting open lock again
if (closed) {
// always add again if back pressure was applied in case
// the bundle file got removed while releasing the open lock
if (needBackPressure) {
mruListAdd();
}
// This can throw an IO exception resulting in closed remaining true on exit
zipFile = BundleFile.secureAction.getZipFile(this.basefile);
closed = false;
}
} else {
mruListUse();
}
return zipFile;
} finally {
if (!keepLock || closed) {
openLock.unlock();
}
}
}
/**
* Returns a ZipEntry for the bundle file. Must be called while holding the open lock.
* This method does not ensure that the ZipFile is opened. Callers may need to call getZipfile() prior to calling this
* method.
* @param path the path to an entry
* @return a ZipEntry or null if the entry does not exist
*/
private ZipEntry getZipEntry(String path) {
if (path.length() > 0 && path.charAt(0) == '/')
path = path.substring(1);
ZipEntry entry = zipFile.getEntry(path);
if (entry != null && entry.getSize() == 0 && !entry.isDirectory()) {
// work around the directory bug see bug 83542
ZipEntry dirEntry = zipFile.getEntry(path + '/');
if (dirEntry != null)
entry = dirEntry;
}
return entry;
}
/**
* Extracts a directory and all sub content to disk
* @param dirName the directory name to extract
* @return the File used to extract the content to. A value
* of <code>null</code> is returned if the directory to extract does
* not exist or if content extraction is not supported.
*/
File extractDirectory(String dirName) {
if (!lockOpen()) {
return null;
}
try {
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
String entryPath = entries.nextElement().getName();
if (entryPath.startsWith(dirName) && !entryPath.endsWith("/")) //$NON-NLS-1$
getFile(entryPath, false);
}
return getExtractFile(dirName);
} finally {
openLock.unlock();
}
}
private File getExtractFile(String entryName) {
if (generation == null)
return null;
return generation.getExtractFile(".cp", entryName); //$NON-NLS-1$
}
public File getFile(String entry, boolean nativeCode) {
if (!lockOpen()) {
return null;
}
try {
ZipEntry zipEntry = getZipEntry(entry);
if (zipEntry == null)
return null;
try {
File nested = getExtractFile(zipEntry.getName());
if (nested != null) {
if (nested.exists()) {
/* the entry is already cached */
if (debug.DEBUG_BUNDLE_FILE)
Debug.println("File already present: " + nested.getPath()); //$NON-NLS-1$
if (nested.isDirectory())
// must ensure the complete directory is extracted (bug 182585)
extractDirectory(zipEntry.getName());
} else {
if (zipEntry.getName().endsWith("/")) { //$NON-NLS-1$
nested.mkdirs();
if (!nested.isDirectory()) {
if (debug.DEBUG_BUNDLE_FILE)
Debug.println("Unable to create directory: " + nested.getPath()); //$NON-NLS-1$
throw new IOException(NLS.bind(Msg.ADAPTOR_DIRECTORY_CREATE_EXCEPTION, nested.getAbsolutePath()));
}
extractDirectory(zipEntry.getName());
} else {
InputStream in = zipFile.getInputStream(zipEntry);
if (in == null)
return null;
generation.storeContent(nested, in, nativeCode);
}
}
return nested;
}
} catch (IOException e) {
if (debug.DEBUG_BUNDLE_FILE)
Debug.printStackTrace(e);
generation.getBundleInfo().getStorage().getLogServices().log(EquinoxContainer.NAME, FrameworkLogEntry.ERROR, "Unable to extract content: " + generation.getRevision() + ": " + entry, e); //$NON-NLS-1$ //$NON-NLS-2$
} catch (StorageException e) {
if (debug.DEBUG_BUNDLE_FILE)
Debug.printStackTrace(e);
generation.getBundleInfo().getStorage().getLogServices().log(EquinoxContainer.NAME, FrameworkLogEntry.ERROR, "Unable to extract content: " + generation.getRevision() + ": " + entry, e); //$NON-NLS-1$ //$NON-NLS-2$
}
} finally {
openLock.unlock();
}
return null;
}
public boolean containsDir(String dir) {
if (!lockOpen()) {
return false;
}
try {
if (dir == null)
return false;
if (dir.length() == 0)
return true;
if (dir.charAt(0) == '/') {
if (dir.length() == 1)
return true;
dir = dir.substring(1);
}
if (dir.length() > 0 && dir.charAt(dir.length() - 1) != '/')
dir = dir + '/';
Enumeration<? extends ZipEntry> entries = zipFile.entries();
ZipEntry zipEntry;
String entryPath;
while (entries.hasMoreElements()) {
zipEntry = entries.nextElement();
entryPath = zipEntry.getName();
if (entryPath.startsWith(dir)) {
return true;
}
}
} finally {
openLock.unlock();
}
return false;
}
public BundleEntry getEntry(String path) {
if (!lockOpen()) {
return null;
}
try {
ZipEntry zipEntry = getZipEntry(path);
if (zipEntry == null) {
if (path.length() == 0 || path.charAt(path.length() - 1) == '/') {
// this is a directory request lets see if any entries exist in this directory
if (containsDir(path))
return new DirZipBundleEntry(this, path);
}
return null;
}
return new ZipBundleEntry(zipEntry, this);
} finally {
openLock.unlock();
}
}
@Override
public Enumeration<String> getEntryPaths(String path, boolean recurse) {
if (!lockOpen()) {
return null;
}
try {
if (path == null)
throw new NullPointerException();
// Strip any leading '/' off of path.
if (path.length() > 0 && path.charAt(0) == '/')
path = path.substring(1);
// Append a '/', if not already there, to path if not an empty string.
if (path.length() > 0 && path.charAt(path.length() - 1) != '/')
path = new StringBuilder(path).append("/").toString(); //$NON-NLS-1$
LinkedHashSet<String> result = new LinkedHashSet<>();
// Get all zip file entries and add the ones of interest.
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry zipEntry = entries.nextElement();
String entryPath = zipEntry.getName();
// Is the entry of possible interest? Note that
// string.startsWith("") == true.
if (entryPath.startsWith(path)) {
// If we get here, we know that the entry is either (1) equal to
// path, (2) a file under path, or (3) a subdirectory of path.
if (path.length() < entryPath.length()) {
// If we get here, we know that entry is not equal to path.
getEntryPaths(path, entryPath.substring(path.length()), recurse, result);
}
}
}
return result.size() == 0 ? null : Collections.enumeration(result);
} finally {
openLock.unlock();
}
}
private void getEntryPaths(String path, String entry, boolean recurse, LinkedHashSet<String> entries) {
if (entry.length() == 0)
return;
int slash = entry.indexOf('/');
if (slash == -1)
entries.add(path + entry);
else {
path = path + entry.substring(0, slash + 1);
entries.add(path);
if (recurse)
getEntryPaths(path, entry.substring(slash + 1), true, entries);
}
}
public void close() throws IOException {
openLock.lock();
try {
if (!closed) {
if (referenceCount > 0 && isMruListClosing()) {
// there are some opened streams to this BundleFile still;
// wait for them all to close because this is being closed by the MRUBundleFileList
try {
refCondition.await(1000, TimeUnit.MICROSECONDS); // timeout after 1 second
} catch (InterruptedException e) {
// do nothing for now ...
}
if (referenceCount != 0 || closed)
// either another thread closed the bundle file or we timed waiting for all the reference inputstreams to close
// If the referenceCount did not reach zero then this bundle file will remain open until the
// bundle file is closed explicitly (i.e. bundle is updated/uninstalled or framework is shutdown)
return;
}
closed = true;
zipFile.close();
mruListRemove();
zipFile = null;
}
} finally {
openLock.unlock();
}
}
private boolean isMruListClosing() {
return this.mruList != null && this.mruList.isClosing(this);
}
private boolean isMruEnabled() {
return this.mruList != null && this.mruList.isEnabled();
}
private void mruListRemove() {
if (this.mruList != null) {
this.mruList.remove(this);
}
}
private void mruListUse() {
if (this.mruList != null) {
mruList.use(this);
}
}
private void mruListApplyBackPressure() {
if (this.mruList != null) {
this.mruList.applyBackpressure();
}
}
private boolean mruListAdd() {
if (this.mruList != null) {
return mruList.add(this);
}
return false;
}
public void open() throws IOException {
getZipFile(false);
}
void incrementReference() {
openLock.lock();
try {
referenceCount += 1;
} finally {
openLock.unlock();
}
}
void decrementReference() {
openLock.lock();
try {
referenceCount = Math.max(0, referenceCount - 1);
// only notify if the referenceCount is zero.
if (referenceCount == 0)
refCondition.signal();
} finally {
openLock.unlock();
}
}
InputStream getInputStream(ZipEntry entry) throws IOException {
if (!lockOpen()) {
throw new IOException("Failed to open zip file."); //$NON-NLS-1$
}
try {
InputStream zipStream = zipFile.getInputStream(entry);
if (isMruEnabled()) {
zipStream = new ZipBundleEntryInputStream(zipStream);
}
return zipStream;
} finally {
openLock.unlock();
}
}
private class ZipBundleEntryInputStream extends FilterInputStream {
private boolean streamClosed = false;
public ZipBundleEntryInputStream(InputStream stream) {
super(stream);
incrementReference();
}
public int available() throws IOException {
try {
return super.available();
} catch (IOException e) {
throw enrichExceptionWithBaseFile(e);
}
}
public void close() throws IOException {
try {
super.close();
} catch (IOException e) {
throw enrichExceptionWithBaseFile(e);
} finally {
synchronized (this) {
if (streamClosed)
return;
streamClosed = true;
}
decrementReference();
}
}
public int read() throws IOException {
try {
return super.read();
} catch (IOException e) {
throw enrichExceptionWithBaseFile(e);
}
}
public int read(byte[] var0, int var1, int var2) throws IOException {
try {
return super.read(var0, var1, var2);
} catch (IOException e) {
throw enrichExceptionWithBaseFile(e);
}
}
public int read(byte[] var0) throws IOException {
try {
return super.read(var0);
} catch (IOException e) {
throw enrichExceptionWithBaseFile(e);
}
}
public void reset() throws IOException {
try {
super.reset();
} catch (IOException e) {
throw enrichExceptionWithBaseFile(e);
}
}
public long skip(long var0) throws IOException {
try {
return super.skip(var0);
} catch (IOException e) {
throw enrichExceptionWithBaseFile(e);
}
}
private IOException enrichExceptionWithBaseFile(IOException e) {
return new IOException(getBaseFile().toString(), e);
}
}
}