| /******************************************************************************* |
| * 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); |
| } |
| } |
| } |