| /******************************************************************************* |
| * 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 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 manages the number of open bundle files by using the MRUBundleFileList |
| * @param <E> a type specified by extending classes to call {@link #getInputStream(Object)} |
| */ |
| public abstract class CloseableBundleFile<E> extends BundleFile { |
| |
| // A reentrant lock is used here (instead of intrinsic synchronization) |
| // to allow the lock conditional held |
| // see lockOpen() and open(boolean) |
| private final ReentrantLock openLock = new ReentrantLock(); |
| private final Condition refCondition = openLock.newCondition(); |
| |
| private final MRUBundleFileList mruList; |
| |
| protected final BundleInfo.Generation generation; |
| |
| protected final Debug debug; |
| |
| /** |
| * The closed flag |
| */ |
| private volatile boolean closed = true; |
| |
| private int referenceCount = 0; |
| |
| public CloseableBundleFile(File basefile, BundleInfo.Generation generation, MRUBundleFileList mruList, Debug debug) { |
| super(basefile); |
| this.debug = debug; |
| this.generation = generation; |
| this.closed = true; |
| this.mruList = mruList; |
| } |
| |
| /** |
| * Checks if the bundle file is open |
| * @return true if the bundle file is open and locked |
| */ |
| protected boolean lockOpen() { |
| try { |
| open(true); |
| return true; |
| } 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; |
| } |
| } |
| |
| /** |
| * Unlocks the open lock |
| */ |
| protected void releaseOpen() { |
| openLock.unlock(); |
| } |
| |
| /** |
| * Opens this bundle file. |
| * @param keepLock true if the open lock should be retained |
| * @throws IOException |
| */ |
| private void open(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 |
| doOpen(); |
| closed = false; |
| if (debug.DEBUG_BUNDLE_FILE_OPEN) { |
| Debug.println("OPENED bundle file - " + toString()); //$NON-NLS-1$ |
| } |
| } |
| } else { |
| mruListUse(); |
| } |
| } finally { |
| if (!keepLock || closed) { |
| openLock.unlock(); |
| } |
| } |
| } |
| |
| /** |
| * Opens the bundle file |
| * @throws IOException if an error occurs |
| */ |
| protected abstract void doOpen() throws IOException; |
| |
| /** |
| * 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 { |
| for (String path : getPaths()) { |
| if (path.startsWith(dirName) && !path.endsWith("/")) //$NON-NLS-1$ |
| getFile(path, false); |
| } |
| return getExtractFile(dirName); |
| } finally { |
| releaseOpen(); |
| } |
| } |
| |
| protected abstract Iterable<String> getPaths(); |
| |
| private File getExtractFile(String entryName) { |
| if (generation == null) |
| return null; |
| return generation.getExtractFile(".cp", entryName); //$NON-NLS-1$ |
| } |
| |
| @Override |
| public File getFile(String entry, boolean nativeCode) { |
| if (generation == null) { |
| return null; |
| } |
| if (!lockOpen()) { |
| return null; |
| } |
| try { |
| BundleEntry bEntry = getEntry(entry); |
| if (bEntry == null) |
| return null; |
| |
| try { |
| File nested = getExtractFile(bEntry.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(bEntry.getName()); |
| } else { |
| if (bEntry.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(bEntry.getName()); |
| } else { |
| InputStream in = bEntry.getInputStream(); |
| if (in == null) |
| return null; |
| generation.storeContent(nested, in, nativeCode); |
| } |
| } |
| |
| return nested; |
| } |
| } catch (IOException | 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 { |
| releaseOpen(); |
| } |
| return null; |
| } |
| |
| @Override |
| 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 + '/'; |
| |
| for (String entry : getPaths()) { |
| if (entry.startsWith(dir)) { |
| return true; |
| } |
| } |
| } finally { |
| releaseOpen(); |
| } |
| return false; |
| } |
| |
| @Override |
| public BundleEntry getEntry(String path) { |
| if (!lockOpen()) { |
| return null; |
| } |
| try { |
| return findEntry(path); |
| } finally { |
| releaseOpen(); |
| } |
| } |
| |
| /** |
| * Finds the bundle entry for the specified path |
| * @param path the path of the entry to find |
| * @return the entry or {@code null} if no entry exists |
| */ |
| protected abstract BundleEntry findEntry(String path); |
| |
| @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 entries and add the ones of interest. |
| for (String entryPath : getPaths()) { |
| // 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 { |
| releaseOpen(); |
| } |
| } |
| |
| 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); |
| } |
| } |
| |
| @Override |
| 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; |
| doClose(); |
| mruListRemove(); |
| postClose(); |
| if (debug.DEBUG_BUNDLE_FILE_CLOSE) { |
| Debug.println("CLOSED bundle file - " + toString()); //$NON-NLS-1$ |
| } |
| } |
| } finally { |
| openLock.unlock(); |
| } |
| } |
| |
| /** |
| * Closes the bundle file |
| * @throws IOException if an error occurs closing |
| */ |
| protected abstract void doClose() throws IOException; |
| |
| /** |
| * Called after closing the bundle file. |
| */ |
| protected abstract void postClose(); |
| |
| 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; |
| } |
| |
| @Override |
| public void open() throws IOException { |
| open(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(); |
| } |
| } |
| |
| /** |
| * Gets the input stream for the specified entry. |
| * This method will ensure the bundle file is open, |
| * call {@link #doGetInputStream(Object)} to get the |
| * actual input stream, then if the bundle file limit |
| * is enabled it will wrapper the input stream in a |
| * special input stream that keeps track of active |
| * input streams to prevent the bundle file from being |
| * closed until the stream is closed (or a timeout happens). |
| * @param entry the entry to get the input stream for |
| * @return the input stream for the entry |
| * @throws IOException |
| */ |
| public InputStream getInputStream(E entry) throws IOException { |
| if (!lockOpen()) { |
| throw new IOException("Failed to lock bundle file."); //$NON-NLS-1$ |
| } |
| try { |
| InputStream in = doGetInputStream(entry); |
| if (isMruEnabled()) { |
| in = new BundleEntryInputStream(in); |
| } |
| return in; |
| } finally { |
| releaseOpen(); |
| } |
| } |
| |
| /** |
| * Gets the input stream for the specified entry. |
| * @param entry the entry to get the input stream for. The type is specified by the |
| * extending class. |
| * @return the input steam for the entry |
| * @throws IOException if an error occurs |
| */ |
| protected abstract InputStream doGetInputStream(E entry) throws IOException; |
| |
| private class BundleEntryInputStream extends FilterInputStream { |
| |
| private boolean streamClosed = false; |
| |
| public BundleEntryInputStream(InputStream stream) { |
| super(stream); |
| incrementReference(); |
| } |
| |
| @Override |
| public int available() throws IOException { |
| try { |
| return super.available(); |
| } catch (IOException e) { |
| throw enrichExceptionWithBaseFile(e); |
| } |
| } |
| |
| @Override |
| public void close() throws IOException { |
| try { |
| super.close(); |
| } catch (IOException e) { |
| throw enrichExceptionWithBaseFile(e); |
| } finally { |
| synchronized (this) { |
| if (streamClosed) |
| return; |
| streamClosed = true; |
| } |
| decrementReference(); |
| } |
| } |
| |
| @Override |
| public int read() throws IOException { |
| try { |
| return super.read(); |
| } catch (IOException e) { |
| throw enrichExceptionWithBaseFile(e); |
| } |
| } |
| |
| @Override |
| public int read(byte[] var0, int var1, int var2) throws IOException { |
| try { |
| return super.read(var0, var1, var2); |
| } catch (IOException e) { |
| throw enrichExceptionWithBaseFile(e); |
| } |
| } |
| |
| @Override |
| public int read(byte[] var0) throws IOException { |
| try { |
| return super.read(var0); |
| } catch (IOException e) { |
| throw enrichExceptionWithBaseFile(e); |
| } |
| } |
| |
| @Override |
| public void reset() throws IOException { |
| try { |
| super.reset(); |
| } catch (IOException e) { |
| throw enrichExceptionWithBaseFile(e); |
| } |
| } |
| |
| @Override |
| public long skip(long var0) throws IOException { |
| try { |
| return super.skip(var0); |
| } catch (IOException e) { |
| throw enrichExceptionWithBaseFile(e); |
| } |
| } |
| |
| private IOException enrichExceptionWithBaseFile(IOException e) { |
| File baseFile = getBaseFile(); |
| String extraInfo = baseFile == null ? // |
| generation == null ? null : generation.getBundleInfo().getLocation() : // |
| baseFile.toString(); |
| return new IOException(extraInfo, e); |
| } |
| } |
| } |