blob: 8e63af981f07ec618e4520d7e8d3b72177faee9b [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.cache;
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;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.SoftReference;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* <p>
* This class, combined with <tt>BundleArchive</tt>, and concrete
* <tt>BundleRevision</tt> subclasses, implement the Felix bundle cache.
* It is possible to configure the default behavior of this class by
* passing properties into Felix' constructor. The configuration properties
* for this class are (properties starting with "<tt>felix</tt>" are specific
* to Felix, while those starting with "<tt>org.osgi</tt>" are standard OSGi
* properties):
* </p>
* <ul>
* <li><tt>felix.cache.filelimit</tt> - The integer value of this string
* sets an upper limit on how many files the cache will open. The default
* value is zero, which means there is no limit.
* </li>
* <li><tt>org.osgi.framework.storage</tt> - Sets the directory to use as
* the bundle cache; by default bundle cache directory is
* <tt>felix-cache</tt> in the current working directory. The value
* should be a valid directory name. The directory name can be either absolute
* or relative. Relative directory names are relative to the current working
* directory. The specified directory will be created if it does
* not exist.
* </li>
* <li><tt>felix.cache.rootdir</tt> - Sets the root directory to use to
* calculate the bundle cache directory for relative directory names. If
* <tt>org.osgi.framework.storage</tt> is set to a relative name, by
* default it is relative to the current working directory. If this
* property is set, then it will be calculated as being relative to
* the specified root directory.
* </li>
* <li><tt>felix.cache.locking</tt> - Enables or disables bundle cache locking,
* which is used to prevent concurrent access to the bundle cache. This is
* enabled by default, but on older/smaller JVMs file channel locking is
* not available; set this property to <tt>false</tt> to disable it.
* </li>
* <li><tt>felix.cache.bufsize</tt> - Sets the buffer size to be used by
* the cache; the default value is 4096. The integer value of this
* string provides control over the size of the internal buffer of the
* disk cache for performance reasons.
* </li>
* <p>
* For specific information on how to configure the Felix framework, refer
* to the Felix framework usage documentation.
* </p>
* @see org.apache.felix.framework.cache.BundleArchive
**/
public class BundleCache
{
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";
private static final ThreadLocal m_defaultBuffer = new ThreadLocal();
private static volatile int DEFAULT_BUFFER = 1024 * 64;
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)
throws Exception
{
m_logger = logger;
m_configMap = configMap;
int limit = 0;
String limitStr = (String) m_configMap.get(CACHE_FILELIMIT_PROP);
if (limitStr != null)
{
try
{
limit = Integer.parseInt(limitStr);
}
catch (NumberFormatException ex)
{
limit = 0;
}
}
m_zipFactory = new WeakZipFileFactory(limit);
// Create the cache directory, if it does not exist.
File cacheDir = determineCacheDir(m_configMap);
if (!getSecureAction().fileExists(cacheDir))
{
if (!getSecureAction().mkdirs(cacheDir))
{
m_logger.log(
Logger.LOG_ERROR,
"Unable to create cache directory: " + cacheDir);
throw new RuntimeException("Unable to create cache directory.");
}
}
Object locking = m_configMap.get(CACHE_LOCKING_PROP);
locking = (locking == null)
? Boolean.TRUE.toString()
: locking.toString().toLowerCase();
if (locking.equals(Boolean.TRUE.toString()))
{
File lockFile = new File(cacheDir, CACHE_LOCK_NAME);
FileChannel fc = null;
FileOutputStream fos = null;
try
{
if (!getSecureAction().fileExists(lockFile))
{
fos = getSecureAction().getFileOutputStream(lockFile);
fc = fos.getChannel();
}
else
{
fos = getSecureAction().getFileOutputStream(lockFile);
fc = fos.getChannel();
}
}
catch (Exception ex)
{
try
{
if (fos != null) fos.close();
if (fc != null) fc.close();
}
catch (Exception ex2)
{
// Ignore.
}
throw new Exception("Unable to create bundle cache lock file: " + ex);
}
try
{
m_lock = fc.tryLock();
}
catch (Exception ex)
{
throw new Exception("Unable to lock bundle cache: " + ex);
}
}
else
{
m_lock = null;
}
}
// Parse the main attributes of the manifest of the given jarfile.
// 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.
//
// @return the given map for convenience
public static Map<String, Object> getMainAttributes(Map<String, Object> headers, InputStream inputStream, long size) throws Exception
{
if (size > 0)
{
return getMainAttributes(headers, inputStream, (int) (size < Integer.MAX_VALUE ? size : Integer.MAX_VALUE));
}
else
{
return headers;
}
}
static byte[] read(InputStream input, long size) throws Exception
{
return read(input, size <= Integer.MAX_VALUE ? (int) size : Integer.MAX_VALUE);
}
static byte[] read(InputStream input, int size) throws Exception
{
if (size <= 0)
{
return new byte[0];
}
byte[] result = new byte[size];
Exception exception = null;
try
{
for (int i = input.read(result, 0, size); i != -1 && i < size; i += input.read(result, i, size - i))
{
}
}
catch (Exception ex)
{
exception = ex;
}
finally
{
try
{
input.close();
}
catch (Exception ex)
{
throw exception != null ? exception : ex;
}
}
return result;
}
public static Map<String, Object> getMainAttributes(Map<String, Object> headers, InputStream inputStream, int size) throws Exception
{
if (size <= 0)
{
inputStream.close();
return headers;
}
// Get the buffer for this thread if there is one already otherwise,
// create one of size DEFAULT_BUFFER (64K) if the manifest is less
// than 64k or of the size of the manifest.
SoftReference ref = (SoftReference) m_defaultBuffer.get();
byte[] bytes = null;
if (ref != null)
{
bytes = (byte[]) ref.get();
}
if (bytes == null)
{
bytes = new byte[size + 1 > DEFAULT_BUFFER ? size + 1 : DEFAULT_BUFFER];
m_defaultBuffer.set(new SoftReference(bytes));
}
else if (size + 1 > bytes.length)
{
bytes = new byte[size + 1];
m_defaultBuffer.set(new SoftReference(bytes));
}
// Now read in the manifest in one go into the bytes array.
// The InputStream should be already buffered and can handle up to 64K buffers in one go.
try
{
int i = inputStream.read(bytes);
while (i < size)
{
i += inputStream.read(bytes, i, bytes.length - i);
}
}
finally
{
inputStream.close();
}
// Force a new line at the end of the manifest to deal with broken manifest without any line-ending
bytes[size++] = '\n';
// Now parse the main attributes. The idea is to do that
// without creating new byte arrays. Therefore, we read through
// the manifest bytes inside the bytes array and write them back into
// the same array unless we don't need them (e.g., \r\n and \n are skipped).
// That allows us to create the strings from the bytes array without the skipped
// chars. We stop as soon as we see a blank line as that denotes that the main
// attributes part is finished.
String key = null;
int last = 0;
int current = 0;
for (int i = 0; i < size; i++)
{
// skip \r and \n if it is followed by another \n
// (we catch the blank line case in the next iteration)
if (bytes[i] == '\r')
{
if ((i + 1 < size) && (bytes[i + 1] == '\n'))
{
continue;
}
}
if (bytes[i] == '\n')
{
if ((i + 1 < size) && (bytes[i + 1] == ' '))
{
i++;
continue;
}
}
// If we don't have a key yet and see the first : we parse it as the key
// and skip the :<blank> that follows it.
if ((key == null) && (bytes[i] == ':'))
{
key = new String(bytes, last, (current - last), "UTF-8");
if ((i + 1 < size) && (bytes[i + 1] == ' '))
{
last = current + 1;
continue;
}
else
{
throw new Exception("Manifest error: Missing space separator - " + key);
}
}
// if we are at the end of a line
if (bytes[i] == '\n')
{
// and it is a blank line stop parsing (main attributes are done)
if ((last == current) && (key == null))
{
break;
}
// Otherwise, parse the value and add it to the map (we throw an
// exception if we don't have a key or the key already exist.
String value = new String(bytes, last, (current - last), "UTF-8");
if (key == null)
{
throw new Exception("Manifest error: Missing attribute name - " + value);
}
else if (headers.put(key.intern(), value) != null)
{
throw new Exception("Manifest error: Duplicate attribute name - " + key);
}
last = current;
key = null;
}
else
{
// write back the byte if it needs to be included in the key or the value.
bytes[current++] = bytes[i];
}
}
return headers;
}
public synchronized void release()
{
if (m_lock != null)
{
try
{
((FileLock) m_lock).release();
((FileLock) m_lock).channel().close();
}
catch (Exception ex)
{
// Not much we can do here, just log it.
m_logger.log(
Logger.LOG_WARNING,
"Exception releasing bundle cache.", ex);
}
}
}
/* package */ static SecureAction getSecureAction()
{
return m_secureAction;
}
public synchronized void delete() throws Exception
{
// Delete the cache directory.
File cacheDir = determineCacheDir(m_configMap);
deleteDirectoryTree(cacheDir);
}
public BundleArchive[] getArchives()
throws Exception
{
// Get buffer size value.
try
{
String sBufSize = (String) m_configMap.get(CACHE_BUFSIZE_PROP);
if (sBufSize != null)
{
DEFAULT_BUFFER = Integer.parseInt(sBufSize);
}
}
catch (NumberFormatException ne)
{
// Use the default value.
}
// Create the existing bundle archives in the directory, if any exist.
File cacheDir = determineCacheDir(m_configMap);
List archiveList = new ArrayList();
File[] children = getSecureAction().listDirectory(cacheDir);
for (int i = 0; (children != null) && (i < children.length); i++)
{
// Ignore directories that aren't bundle directories or
// is the system bundle directory.
if (children[i].getName().startsWith(BUNDLE_DIR_PREFIX) &&
!children[i].getName().equals(BUNDLE_DIR_PREFIX + Long.toString(0)))
{
// Recreate the bundle archive.
try
{
archiveList.add(
new BundleArchive(
m_logger, m_configMap, m_zipFactory, children[i]));
}
catch (Exception ex)
{
// Log exception and remove bundle archive directory.
m_logger.log(Logger.LOG_ERROR,
"Error reloading cached bundle, removing it: " + children[i], ex);
deleteDirectoryTree(children[i]);
}
}
}
return (BundleArchive[])
archiveList.toArray(new BundleArchive[archiveList.size()]);
}
public BundleArchive create(long id, int startLevel, String location, InputStream is)
throws Exception
{
File cacheDir = determineCacheDir(m_configMap);
// Construct archive root directory.
File archiveRootDir =
new File(cacheDir, BUNDLE_DIR_PREFIX + Long.toString(id));
try
{
// Create the archive and add it to the list of archives.
BundleArchive ba =
new BundleArchive(
m_logger, m_configMap, m_zipFactory, archiveRootDir,
id, startLevel, location, is);
return ba;
}
catch (Exception ex)
{
if (m_secureAction.fileExists(archiveRootDir))
{
if (!BundleCache.deleteDirectoryTree(archiveRootDir))
{
m_logger.log(
Logger.LOG_ERROR,
"Unable to delete the archive directory: "
+ archiveRootDir);
}
}
throw ex;
}
}
/**
* Provides the system bundle access to its private storage area; this
* special case is necessary since the system bundle is not really a
* bundle and therefore must be treated in a special way.
* @param fileName the name of the file in the system bundle's private area.
* @return a <tt>File</tt> object corresponding to the specified file name.
* @throws Exception if any error occurs.
**/
public File getSystemBundleDataFile(String fileName)
throws Exception
{
// Make sure system bundle directory exists.
File sbDir = new File(determineCacheDir(m_configMap), BUNDLE_DIR_PREFIX + Long.toString(0));
// If the system bundle directory exists, then we don't
// need to initialize since it has already been done.
if (!getSecureAction().fileExists(sbDir) && !getSecureAction().mkdirs(sbDir) && !getSecureAction().fileExists(sbDir))
{
m_logger.log(
Logger.LOG_ERROR,
"Unable to create system bundle directory.");
throw new IOException("Unable to create system bundle directory.");
}
File dataFile = new File(sbDir, fileName);
String dataFilePath = BundleCache.getSecureAction().getCanonicalPath(dataFile);
String dataDirPath = BundleCache.getSecureAction().getCanonicalPath(sbDir);
if (!dataFilePath.equals(dataDirPath) && !dataFilePath.startsWith(dataDirPath + File.separatorChar))
{
throw new IllegalArgumentException("The data file must be inside the data dir.");
}
return dataFile;
}
//
// Static file-related utility methods.
//
/**
* This method copies an input stream to the specified file.
* @param is the input stream to copy.
* @param outputFile the file to which the input stream should be copied.
**/
static void copyStreamToFile(InputStream is, File outputFile)
throws IOException
{
// Get the buffer for this thread if there is one already otherwise,
// create one of size DEFAULT_BUFFER
SoftReference ref = (SoftReference) m_defaultBuffer.get();
byte[] bytes = null;
if (ref != null)
{
bytes = (byte[]) ref.get();
}
if (bytes == null)
{
bytes = new byte[DEFAULT_BUFFER];
m_defaultBuffer.set(new SoftReference(bytes));
}
OutputStream os = null;
try
{
os = getSecureAction().getFileOutputStream(outputFile);
for (int i = is.read(bytes);i != -1; i = is.read(bytes))
{
os.write(bytes, 0, i);
}
}
finally
{
try
{
if (is != null) is.close();
}
finally
{
if (os != null) os.close();
}
}
}
static boolean deleteDirectoryTree(File target)
{
if (!deleteDirectoryTreeRecursive(target))
{
// 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.
System.gc();
System.gc();
return deleteDirectoryTreeRecursive(target);
}
return true;
}
//
// Private methods.
//
private static File determineCacheDir(Map configMap)
{
File cacheDir;
// Check to see if the cache directory is specified in the storage
// configuration property.
String cacheDirStr = (String) configMap.get(Constants.FRAMEWORK_STORAGE);
// Get the cache root directory for relative paths; the default is ".".
String rootDirStr = (String) configMap.get(CACHE_ROOTDIR_PROP);
rootDirStr = (rootDirStr == null) ? CACHE_ROOTDIR_DEFAULT : rootDirStr;
if (cacheDirStr != null)
{
// If the specified cache directory is relative, then use the
// root directory to calculate the absolute path.
cacheDir = new File(cacheDirStr);
if (!cacheDir.isAbsolute())
{
cacheDir = new File(rootDirStr, cacheDirStr);
}
}
else
{
// If no cache directory was specified, then use the default name
// in the root directory.
cacheDir = new File(rootDirStr, CACHE_DIR_NAME);
}
return cacheDir;
}
private static boolean deleteDirectoryTreeRecursive(File target)
{
if (!getSecureAction().fileExists(target))
{
return true;
}
if (getSecureAction().isFileDirectory(target))
{
File[] files = getSecureAction().listDirectory(target);
if (files != null)
{
for (int i = 0; i < files.length; i++)
{
deleteDirectoryTreeRecursive(files[i]);
}
}
}
return getSecureAction().deleteFile(target);
}
}