blob: 7829e4268dd5fa1a4eec6aa320d890deebd735d3 [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 java.io.*;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import org.apache.felix.framework.Logger;
import org.apache.felix.framework.util.WeakZipFileFactory;
import org.osgi.framework.Bundle;
import org.osgi.framework.Constants;
/**
* <p>
* This class is a logical abstraction for a bundle archive. This class,
* combined with <tt>BundleCache</tt> and concrete <tt>BundleRevision</tt>
* subclasses, implement the bundle cache for Felix. The bundle archive
* abstracts the actual bundle content into revisions and the revisions
* provide access to the actual bundle content. When a bundle is
* installed it has one revision associated with its content. Updating a
* bundle adds another revision for the updated content. Any number of
* revisions can be associated with a bundle archive. When the bundle
* (or framework) is refreshed, then all old revisions are purged and only
* the most recent revision is maintained.
* </p>
* <p>
* The content associated with a revision can come in many forms, such as
* a standard JAR file or an exploded bundle directory. The bundle archive
* is responsible for creating all revision instances during invocations
* of the <tt>revise()</tt> method call. Internally, it determines the
* concrete type of revision type by examining the location string as an
* URL. Currently, it supports standard JAR files, referenced JAR files,
* and referenced directories. Examples of each type of URL are, respectively:
* </p>
* <ul>
* <li><tt>http://www.foo.com/bundle.jar</tt></li>
* <li><tt>reference:file:/foo/bundle.jar</tt></li>
* <li><tt>reference:file:/foo/bundle/</tt></li>
* </ul>
* <p>
* The "<tt>reference:</tt>" notation signifies that the resource should be
* used "in place", meaning that they will not be copied. For referenced JAR
* files, some resources may still be copied, such as embedded JAR files or
* native libraries, but for referenced exploded bundle directories, nothing
* will be copied. Currently, reference URLs can only refer to "file:" targets.
* </p>
* @see org.apache.felix.framework.cache.BundleCache
* @see org.apache.felix.framework.cache.BundleRevision
**/
public class BundleArchive
{
public static final transient String FILE_PROTOCOL = "file:";
public static final transient String REFERENCE_PROTOCOL = "reference:";
public static final transient String INPUTSTREAM_PROTOCOL = "inputstream:";
private static final transient String BUNDLE_INFO_FILE = "bundle.info";
private static final transient String REVISION_LOCATION_FILE = "revision.location";
private static final transient String REVISION_DIRECTORY = "version";
private static final transient String DATA_DIRECTORY = "data";
private final Logger m_logger;
private final Map m_configMap;
private final WeakZipFileFactory m_zipFactory;
private final File m_archiveRootDir;
private final boolean m_isSingleBundleFile;
private long m_id = -1;
private String m_originalLocation = null;
private int m_persistentState = -1;
private int m_startLevel = -1;
private long m_lastModified = -1;
/**
* The refresh count field is used when generating the bundle revision
* directory name where native libraries are extracted. This is necessary
* because Sun's JVM requires a one-to-one mapping between native libraries
* and class loaders where the native library is uniquely identified by its
* absolute path in the file system. This constraint creates a problem when
* a bundle is refreshed, because it gets a new class loader. Using the
* refresh counter to generate the name of the bundle revision directory
* resolves this problem because each time bundle is refresh, the native
* library will have a unique name. As a result of the unique name, the JVM
* will then reload the native library without a problem.
**/
private long m_refreshCount = -1;
// Maps a Long revision number to a BundleRevision.
private final SortedMap<Long, BundleArchiveRevision> m_revisions
= new TreeMap<Long, BundleArchiveRevision>();
/**
* <p>
* This constructor is used for creating new archives when a bundle is
* installed into the framework. Each archive receives a logger, a root
* directory, its associated bundle identifier, the associated bundle
* location string, and an input stream from which to read the bundle
* content. The root directory is where any required state can be
* stored. The input stream may be null, in which case the location is
* used as an URL to the bundle content.
* </p>
* @param logger the logger to be used by the archive.
* @param archiveRootDir the archive root directory for storing state.
* @param id the bundle identifier associated with the archive.
* @param location the bundle location string associated with the archive.
* @param is input stream from which to read the bundle content.
* @throws Exception if any error occurs.
**/
public BundleArchive(Logger logger, Map configMap, WeakZipFileFactory zipFactory,
File archiveRootDir, long id, int startLevel, String location, InputStream is)
throws Exception
{
m_logger = logger;
m_configMap = configMap;
m_zipFactory = zipFactory;
m_archiveRootDir = archiveRootDir;
m_id = id;
if (m_id <= 0)
{
throw new IllegalArgumentException(
"Bundle ID cannot be less than or equal to zero.");
}
m_originalLocation = location;
m_persistentState = Bundle.INSTALLED;
m_startLevel = startLevel;
m_lastModified = System.currentTimeMillis();
m_refreshCount = 0;
String s = (String) m_configMap.get(BundleCache.CACHE_SINGLEBUNDLEFILE_PROP);
m_isSingleBundleFile = (s == null) || (!s.equalsIgnoreCase("true")) ? false : true;
// Save state.
initialize();
// Add a revision for the content.
reviseInternal(false, new Long(0), m_originalLocation, is);
}
/**
* <p>
* This constructor is called when an archive for a bundle is being
* reconstructed when the framework is restarted. Each archive receives
* a logger, a root directory, and its associated bundle identifier.
* The root directory is where any required state can be stored.
* </p>
* @param logger the logger to be used by the archive.
* @param archiveRootDir the archive root directory for storing state.
* @param configMap configMap for BundleArchive
* @throws Exception if any error occurs.
**/
public BundleArchive(Logger logger, Map configMap, WeakZipFileFactory zipFactory,
File archiveRootDir)
throws Exception
{
m_logger = logger;
m_configMap = configMap;
m_zipFactory = zipFactory;
m_archiveRootDir = archiveRootDir;
String s = (String) m_configMap.get(BundleCache.CACHE_SINGLEBUNDLEFILE_PROP);
m_isSingleBundleFile = (s == null) || (!s.equalsIgnoreCase("true")) ? false : true;
if (m_isSingleBundleFile)
{
readBundleInfo();
}
// Add a revision number for each revision that exists in the file
// system. The file system might contain more than one revision if
// the bundle was updated in a previous session, but the framework
// was not refreshed; this might happen if the framework did not
// exit cleanly. We must add the existing revisions so that
// they can be properly purged.
// Find the existing revision directories, which will be named like:
// "${REVISION_DIRECTORY)${refresh-count}.${revision-number}"
File[] children = m_archiveRootDir.listFiles();
for (File child : children)
{
if (child.getName().startsWith(REVISION_DIRECTORY)
&& child.isDirectory())
{
// Determine the revision number and add it to the revision map.
int idx = child.getName().lastIndexOf('.');
if (idx > 0)
{
Long revNum = Long.decode(child.getName().substring(idx + 1));
m_revisions.put(revNum, null);
}
}
}
if (m_revisions.isEmpty())
{
throw new Exception(
"No valid revisions in bundle archive directory: "
+ archiveRootDir);
}
// Remove the last revision number since the call to reviseInternal()
// will properly add the most recent bundle revision.
// NOTE: We do not actually need to add a real revision object for the
// older revisions since they will be purged immediately on framework
// startup.
Long currentRevNum = m_revisions.lastKey();
m_revisions.remove(currentRevNum);
// Add the revision object for the most recent revision.
reviseInternal(true, currentRevNum, getRevisionLocation(currentRevNum), null);
}
/**
* <p>
* Returns the bundle identifier associated with this archive.
* </p>
* @return the bundle identifier associated with this archive.
* @throws Exception if any error occurs.
**/
public synchronized long getId() throws Exception
{
if (m_id <= 0)
{
m_id = readId();
}
return m_id;
}
/**
* <p>
* Returns the location string associated with this archive.
* </p>
* @return the location string associated with this archive.
* @throws Exception if any error occurs.
**/
public synchronized String getLocation() throws Exception
{
if (m_originalLocation == null)
{
m_originalLocation = readLocation();
}
return m_originalLocation;
}
/**
* <p>
* Returns the persistent state of this archive. The value returned is
* one of the following: <tt>Bundle.INSTALLED</tt>, <tt>Bundle.ACTIVE</tt>,
* or <tt>Bundle.UNINSTALLED</tt>.
* </p>
* @return the persistent state of this archive.
* @throws Exception if any error occurs.
**/
public synchronized int getPersistentState() throws Exception
{
if (m_persistentState < 0)
{
m_persistentState = readPersistentState();
}
return m_persistentState;
}
/**
* <p>
* Sets the persistent state of this archive. The value is
* one of the following: <tt>Bundle.INSTALLED</tt>, <tt>Bundle.ACTIVE</tt>,
* or <tt>Bundle.UNINSTALLED</tt>.
* </p>
* @param state the persistent state value to set for this archive.
* @throws Exception if any error occurs.
**/
public synchronized void setPersistentState(int state) throws Exception
{
if (m_persistentState != state)
{
m_persistentState = state;
if (m_isSingleBundleFile)
{
writeBundleInfo();
}
else
{
writePersistentState();
}
}
}
/**
* <p>
* Returns the start level of this archive.
* </p>
* @return the start level of this archive.
* @throws Exception if any error occurs.
**/
public synchronized int getStartLevel() throws Exception
{
if (m_startLevel < 0)
{
m_startLevel = readStartLevel();
}
return m_startLevel;
}
/**
* <p>
* Sets the the start level of this archive this archive.
* </p>
* @param level the start level to set for this archive.
* @throws Exception if any error occurs.
**/
public synchronized void setStartLevel(int level) throws Exception
{
if (m_startLevel != level)
{
m_startLevel = level;
if (m_isSingleBundleFile)
{
writeBundleInfo();
}
else
{
writeStartLevel();
}
}
}
/**
* <p>
* Returns the last modification time of this archive.
* </p>
* @return the last modification time of this archive.
* @throws Exception if any error occurs.
**/
public synchronized long getLastModified() throws Exception
{
if (m_lastModified < 0)
{
m_lastModified = readLastModified();
}
return m_lastModified;
}
/**
* <p>
* Sets the the last modification time of this archive.
* </p>
* @param lastModified The time of the last modification to set for
* this archive. According to the OSGi specification this time is
* set each time a bundle is installed, updated or uninstalled.
*
* @throws Exception if any error occurs.
**/
public synchronized void setLastModified(long lastModified) throws Exception
{
if (m_lastModified != lastModified)
{
m_lastModified = lastModified;
if (m_isSingleBundleFile)
{
writeBundleInfo();
}
else
{
writeLastModified();
}
}
}
/**
* This utility method is used to retrieve the current refresh
* counter value for the bundle. This value is used when generating
* the bundle revision directory name where native libraries are extracted.
* This is necessary because Sun's JVM requires a one-to-one mapping
* between native libraries and class loaders where the native library
* is uniquely identified by its absolute path in the file system. This
* constraint creates a problem when a bundle is refreshed, because it
* gets a new class loader. Using the refresh counter to generate the name
* of the bundle revision directory resolves this problem because each time
* bundle is refresh, the native library will have a unique name.
* As a result of the unique name, the JVM will then reload the
* native library without a problem.
**/
private long getRefreshCount() throws Exception
{
// If the refresh counter is not yet initialized, do so now.
if (m_refreshCount < 0)
{
m_refreshCount = readRefreshCount();
}
return m_refreshCount;
}
/**
* This utility method is used to retrieve the current refresh
* counter value for the bundle. This value is used when generating
* the bundle revision directory name where native libraries are extracted.
* This is necessary because Sun's JVM requires a one-to-one mapping
* between native libraries and class loaders where the native library
* is uniquely identified by its absolute path in the file system. This
* constraint creates a problem when a bundle is refreshed, because it
* gets a new class loader. Using the refresh counter to generate the name
* of the bundle revision directory resolves this problem because each time
* bundle is refresh, the native library will have a unique name.
* As a result of the unique name, the JVM will then reload the
* native library without a problem.
**/
private void setRefreshCount(long count)
throws Exception
{
if (m_refreshCount != count)
{
m_refreshCount = count;
if (m_isSingleBundleFile)
{
writeBundleInfo();
}
else
{
writeRefreshCount();
}
}
}
/**
* <p>
* Returns a <tt>File</tt> object corresponding to the data file
* of the relative path of the specified string.
* </p>
* @return a <tt>File</tt> object corresponding to the specified file name.
* @throws Exception if any error occurs.
**/
public synchronized File getDataFile(String fileName) throws Exception
{
// Do some sanity checking.
if ((fileName.length() > 0) && (fileName.charAt(0) == File.separatorChar))
{
throw new IllegalArgumentException(
"The data file path must be relative, not absolute.");
}
else if (fileName.indexOf("..") >= 0)
{
throw new IllegalArgumentException(
"The data file path cannot contain a reference to the \"..\" directory.");
}
// Get bundle data directory.
File dataDir = new File(m_archiveRootDir, DATA_DIRECTORY);
// Create the data directory if necessary.
if (!BundleCache.getSecureAction().fileExists(dataDir))
{
if (!BundleCache.getSecureAction().mkdir(dataDir))
{
throw new IOException("Unable to create bundle data directory.");
}
}
// Return the data file.
return new File(dataDir, fileName);
}
/**
* <p>
* Returns the current revision object for the archive.
* </p>
* @return the current revision object for the archive.
**/
public synchronized Long getCurrentRevisionNumber()
{
return (m_revisions.isEmpty()) ? null : m_revisions.lastKey();
}
/**
* <p>
* Returns the current revision object for the archive.
* </p>
* @return the current revision object for the archive.
**/
public synchronized BundleArchiveRevision getCurrentRevision()
{
return (m_revisions.isEmpty()) ? null : m_revisions.get(m_revisions.lastKey());
}
public synchronized boolean isRemovalPending()
{
return (m_revisions.size() > 1);
}
/**
* <p>
* This method adds a revision to the archive using the associated
* location and input stream. If the input stream is null, then the
* location is used a URL to obtain an input stream.
* </p>
* @param location the location string associated with the revision.
* @param is the input stream from which to read the revision.
* @throws Exception if any error occurs.
**/
public synchronized void revise(String location, InputStream is)
throws Exception
{
Long revNum = (m_revisions.isEmpty())
? new Long(0)
: new Long(m_revisions.lastKey().longValue() + 1);
reviseInternal(false, revNum, location, is);
}
/**
* Actually adds a revision to the bundle archive. This method is also
* used to reload cached bundles too. The revision is given the specified
* revision number and is read from the input stream if supplied or from
* the location URL if not.
* @param isReload if the bundle is being reloaded or not.
* @param revNum the revision number of the revision.
* @param location the location associated with the revision.
* @param is the input stream from which to read the revision.
* @throws Exception if any error occurs.
*/
private void reviseInternal(
boolean isReload, Long revNum, String location, InputStream is)
throws Exception
{
// If we have an input stream, then we have to use it
// no matter what the update location is, so just ignore
// the update location and set the location to be input
// stream.
if (is != null)
{
location = "inputstream:";
}
// Create a bundle revision for revision number.
BundleArchiveRevision revision = createRevisionFromLocation(location, is, revNum);
if (revision == null)
{
throw new Exception("Unable to revise archive.");
}
if (!isReload)
{
setRevisionLocation(location, revNum);
}
// Add new revision to revision map.
m_revisions.put(revNum, revision);
}
/**
* <p>
* This method undoes the previous revision to the archive; this method will
* remove the latest revision from the archive. This method is only called
* when there are problems during an update after the revision has been
* created, such as errors in the update bundle's manifest. This method
* can only be called if there is more than one revision, otherwise there
* is nothing to undo.
* </p>
* @return true if the undo was a success false if there is no previous revision
* @throws Exception if any error occurs.
*/
public synchronized boolean rollbackRevise() throws Exception
{
// Can only undo the revision if there is more than one.
if (m_revisions.size() <= 1)
{
return false;
}
Long revNum = m_revisions.lastKey();
BundleArchiveRevision revision = m_revisions.remove(revNum);
try
{
revision.close();
}
catch(Exception ex)
{
m_logger.log(Logger.LOG_ERROR, getClass().getName() +
": Unable to dispose latest revision", ex);
}
File revisionDir = new File(m_archiveRootDir, REVISION_DIRECTORY +
getRefreshCount() + "." + revNum.toString());
if (BundleCache.getSecureAction().fileExists(revisionDir))
{
BundleCache.deleteDirectoryTree(revisionDir);
}
return true;
}
private synchronized String getRevisionLocation(Long revNum) throws Exception
{
InputStream is = null;
BufferedReader br = null;
try
{
is = BundleCache.getSecureAction().getFileInputStream(new File(
new File(m_archiveRootDir, REVISION_DIRECTORY +
getRefreshCount() + "." + revNum.toString()), REVISION_LOCATION_FILE));
br = new BufferedReader(new InputStreamReader(is));
return br.readLine();
}
finally
{
if (br != null) br.close();
if (is != null) is.close();
}
}
private synchronized void setRevisionLocation(String location, Long revNum)
throws Exception
{
// Save current revision location.
OutputStream os = null;
BufferedWriter bw = null;
try
{
os = BundleCache.getSecureAction()
.getFileOutputStream(new File(
new File(m_archiveRootDir, REVISION_DIRECTORY +
getRefreshCount() + "." + revNum.toString()), REVISION_LOCATION_FILE));
bw = new BufferedWriter(new OutputStreamWriter(os));
bw.write(location, 0, location.length());
}
finally
{
if (bw != null) bw.close();
if (os != null) os.close();
}
}
public synchronized void close()
{
// Get the current revision count.
for (BundleArchiveRevision revision : m_revisions.values())
{
// Dispose of the revision, but this might be null in certain
// circumstances, such as if this bundle archive was created
// for an existing bundle that was updated, but not refreshed
// due to a system crash; see the constructor code for details.
if (revision != null)
{
try
{
revision.close();
}
catch (Exception ex)
{
m_logger.log(
Logger.LOG_ERROR,
"Unable to close revision - "
+ revision.getRevisionRootDir(), ex);
}
}
}
}
/**
* <p>
* This method closes any revisions and deletes the bundle archive directory.
* </p>
* @throws Exception if any error occurs.
**/
public synchronized void closeAndDelete()
{
// Close the revisions and delete the archive directory.
close();
if (!BundleCache.deleteDirectoryTree(m_archiveRootDir))
{
m_logger.log(
Logger.LOG_ERROR,
"Unable to delete archive directory - " + m_archiveRootDir);
}
}
/**
* <p>
* This method removes all old revisions associated with the archive
* and keeps only the current revision.
* </p>
* @throws Exception if any error occurs.
**/
public synchronized void purge() throws Exception
{
// Remember current revision number.
Long currentRevNum = getCurrentRevisionNumber();
// Record whether the current revision has native libraries, which
// we'll use later to determine if we need to rename its directory.
boolean hasNativeLibs = getCurrentRevision().getManifestHeader()
.containsKey(Constants.BUNDLE_NATIVECODE);
// Close all revisions and then delete all but the current revision.
// We don't delete it the current revision, because we want to rename it
// to the new refresh level.
close();
// Delete all old revisions.
long refreshCount = getRefreshCount();
for (Long revNum : m_revisions.keySet())
{
if (!revNum.equals(currentRevNum))
{
File revisionDir = new File(
m_archiveRootDir,
REVISION_DIRECTORY + refreshCount + "." + revNum.toString());
if (BundleCache.getSecureAction().fileExists(revisionDir))
{
BundleCache.deleteDirectoryTree(revisionDir);
}
}
}
// If the revision has native libraries, then rename its directory
// to avoid the issue of being unable to load the same native library
// into two different class loaders.
if (hasNativeLibs)
{
// Increment the refresh count.
setRefreshCount(refreshCount + 1);
// Rename the current revision directory to the new refresh level.
File currentDir = new File(m_archiveRootDir,
REVISION_DIRECTORY + (refreshCount + 1) + "." + currentRevNum.toString());
File revisionDir = new File(m_archiveRootDir,
REVISION_DIRECTORY + refreshCount + "." + currentRevNum.toString());
BundleCache.getSecureAction().renameFile(revisionDir, currentDir);
}
// Clear the revision map since they are all invalid now.
m_revisions.clear();
// Recreate the revision for the current location.
BundleArchiveRevision revision = createRevisionFromLocation(
getRevisionLocation(currentRevNum), null, currentRevNum);
// Add new revision to the revision map.
m_revisions.put(currentRevNum, revision);
}
/**
* <p>
* Initializes the bundle archive object by creating the archive
* root directory and saving the initial state.
* </p>
* @throws Exception if any error occurs.
**/
private void initialize() throws Exception
{
OutputStream os = null;
BufferedWriter bw = null;
try
{
// If the archive directory exists, then we don't
// need to initialize since it has already been done.
if (BundleCache.getSecureAction().fileExists(m_archiveRootDir))
{
return;
}
// Create archive directory, if it does not exist.
if (!BundleCache.getSecureAction().mkdir(m_archiveRootDir))
{
m_logger.log(
Logger.LOG_ERROR,
getClass().getName() + ": Unable to create archive directory.");
throw new IOException("Unable to create archive directory.");
}
if (m_isSingleBundleFile)
{
writeBundleInfo();
}
else
{
writeId();
writeLocation();
writePersistentState();
writeStartLevel();
writeLastModified();
}
}
finally
{
if (bw != null) bw.close();
if (os != null) os.close();
}
}
/**
* <p>
* Creates a revision based on the location string and/or input stream.
* </p>
* @return the location string associated with this archive.
**/
private BundleArchiveRevision createRevisionFromLocation(
String location, InputStream is, Long revNum)
throws Exception
{
// The revision directory is named using the refresh count and
// the revision number. The revision number is an increasing
// counter of the number of times the bundle was revised.
// The refresh count is necessary due to how native libraries
// are handled in Java; needless to say, every time a bundle is
// refreshed we must change the name of its native libraries so
// that we can reload them. Thus, we use the refresh counter as
// a way to change the name of the revision directory to give
// native libraries new absolute names.
File revisionRootDir = new File(m_archiveRootDir,
REVISION_DIRECTORY + getRefreshCount() + "." + revNum.toString());
BundleArchiveRevision result = null;
try
{
// Check if the location string represents a reference URL.
if ((location != null) && location.startsWith(REFERENCE_PROTOCOL))
{
// Reference URLs only support the file protocol.
location = location.substring(REFERENCE_PROTOCOL.length());
if (!location.startsWith(FILE_PROTOCOL))
{
throw new IOException("Reference URLs can only be files: " + location);
}
// Decode any URL escaped sequences.
location = decode(location);
// Make sure the referenced file exists.
File file = new File(location.substring(FILE_PROTOCOL.length()));
if (!BundleCache.getSecureAction().fileExists(file))
{
throw new IOException("Referenced file does not exist: " + file);
}
// If the referenced file is a directory, then create a directory
// revision; otherwise, create a JAR revision with the reference
// flag set to true.
if (BundleCache.getSecureAction().isFileDirectory(file))
{
result = new DirectoryRevision(m_logger, m_configMap,
m_zipFactory, revisionRootDir, location);
}
else
{
result = new JarRevision(m_logger, m_configMap,
m_zipFactory, revisionRootDir, location, true, null);
}
}
else if (location.startsWith(INPUTSTREAM_PROTOCOL))
{
// Assume all input streams point to JAR files.
result = new JarRevision(m_logger, m_configMap,
m_zipFactory, revisionRootDir, location, false, is);
}
else
{
// Anything else is assumed to be a URL to a JAR file.
result = new JarRevision(m_logger, m_configMap,
m_zipFactory, revisionRootDir, location, false, null);
}
}
catch (Exception ex)
{
if (BundleCache.getSecureAction().fileExists(revisionRootDir))
{
if (!BundleCache.deleteDirectoryTree(revisionRootDir))
{
m_logger.log(
Logger.LOG_ERROR,
getClass().getName()
+ ": Unable to delete revision directory - "
+ revisionRootDir);
}
}
throw ex;
}
return result;
}
// Method from Harmony java.net.URIEncoderDecoder (luni subproject)
// used by URI to decode uri components.
private static String decode(String s) throws UnsupportedEncodingException
{
StringBuffer result = new StringBuffer();
ByteArrayOutputStream out = new ByteArrayOutputStream();
for (int i = 0; i < s.length();)
{
char c = s.charAt(i);
if (c == '%')
{
out.reset();
do
{
if ((i + 2) >= s.length())
{
throw new IllegalArgumentException(
"Incomplete % sequence at: " + i);
}
int d1 = Character.digit(s.charAt(i + 1), 16);
int d2 = Character.digit(s.charAt(i + 2), 16);
if ((d1 == -1) || (d2 == -1))
{
throw new IllegalArgumentException("Invalid % sequence ("
+ s.substring(i, i + 3)
+ ") at: " + String.valueOf(i));
}
out.write((byte) ((d1 << 4) + d2));
i += 3;
}
while ((i < s.length()) && (s.charAt(i) == '%'));
result.append(out.toString("UTF-8"));
continue;
}
result.append(c);
i++;
}
return result.toString();
}
private void readBundleInfo() throws Exception
{
File infoFile = new File(m_archiveRootDir, BUNDLE_INFO_FILE);
// Read the bundle start level.
InputStream is = null;
BufferedReader br= null;
try
{
is = BundleCache.getSecureAction()
.getFileInputStream(infoFile);
br = new BufferedReader(new InputStreamReader(is));
// Read id.
m_id = Long.parseLong(br.readLine());
// Read location.
m_originalLocation = br.readLine();
// Read state.
m_persistentState = Integer.parseInt(br.readLine());
// Read start level.
m_startLevel = Integer.parseInt(br.readLine());
// Read last modified.
m_lastModified = Long.parseLong(br.readLine());
// Read refresh count.
m_refreshCount = Long.parseLong(br.readLine());
}
catch (FileNotFoundException ex)
{
// If there wasn't an info file, then maybe this is an old-style
// bundle cache, so try to read the files individually. We can
// delete this eventually.
m_id = readId();
m_originalLocation = readLocation();
m_persistentState = readPersistentState();
m_startLevel = readStartLevel();
m_lastModified = readLastModified();
m_refreshCount = readRefreshCount();
}
finally
{
if (br != null) br.close();
if (is != null) is.close();
}
}
private void writeBundleInfo() throws Exception
{
// Write the bundle start level.
OutputStream os = null;
BufferedWriter bw = null;
try
{
os = BundleCache.getSecureAction()
.getFileOutputStream(new File(m_archiveRootDir, BUNDLE_INFO_FILE));
bw = new BufferedWriter(new OutputStreamWriter(os));
// Write id.
String s = Long.toString(m_id);
bw.write(s, 0, s.length());
bw.newLine();
// Write location.
s = (m_originalLocation == null) ? "" : m_originalLocation;
bw.write(s, 0, s.length());
bw.newLine();
// Write state.
s = Integer.toString(m_persistentState);
bw.write(s, 0, s.length());
bw.newLine();
// Write start level.
s = Integer.toString(m_startLevel);
bw.write(s, 0, s.length());
bw.newLine();
// Write last modified.
s = Long.toString(m_lastModified);
bw.write(s, 0, s.length());
bw.newLine();
// Write refresh count.
s = Long.toString(m_refreshCount);
bw.write(s, 0, s.length());
bw.newLine();
}
catch (IOException ex)
{
m_logger.log(
Logger.LOG_ERROR,
getClass().getName() + ": Unable to cache bundle info - " + ex);
throw ex;
}
finally
{
if (bw != null) bw.close();
if (os != null) os.close();
}
}
//
// Deprecated bundle cache format to be deleted eventually.
//
private static final transient String BUNDLE_ID_FILE = "bundle.id";
private static final transient String BUNDLE_LOCATION_FILE = "bundle.location";
private static final transient String BUNDLE_STATE_FILE = "bundle.state";
private static final transient String BUNDLE_START_LEVEL_FILE = "bundle.startlevel";
private static final transient String BUNDLE_LASTMODIFIED_FILE = "bundle.lastmodified";
private static final transient String REFRESH_COUNTER_FILE = "refresh.counter";
private void writeId() throws Exception
{
OutputStream os = BundleCache.getSecureAction()
.getFileOutputStream(new File(m_archiveRootDir, BUNDLE_ID_FILE));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
bw.write(Long.toString(m_id), 0, Long.toString(m_id).length());
bw.close();
os.close();
}
private long readId() throws Exception
{
long id;
InputStream is = null;
BufferedReader br = null;
try
{
is = BundleCache.getSecureAction()
.getFileInputStream(new File(m_archiveRootDir, BUNDLE_ID_FILE));
br = new BufferedReader(new InputStreamReader(is));
id = Long.parseLong(br.readLine());
}
catch (FileNotFoundException ex)
{
// HACK: Get the bundle identifier from the archive root directory
// name, which is of the form "bundle<id>" where <id> is the bundle
// identifier numbers. This is a hack to deal with old archives that
// did not save their bundle identifier, but instead had it passed
// into them. Eventually, this can be removed.
id = Long.parseLong(
m_archiveRootDir.getName().substring(
BundleCache.BUNDLE_DIR_PREFIX.length()));
}
finally
{
if (br != null) br.close();
if (is != null) is.close();
}
return id;
}
private void writeLocation() throws Exception
{
OutputStream os = BundleCache.getSecureAction()
.getFileOutputStream(new File(m_archiveRootDir, BUNDLE_LOCATION_FILE));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
bw.write(m_originalLocation, 0, m_originalLocation.length());
bw.close();
os.close();
}
private String readLocation() throws Exception
{
InputStream is = null;
BufferedReader br = null;
try
{
is = BundleCache.getSecureAction()
.getFileInputStream(new File(m_archiveRootDir, BUNDLE_LOCATION_FILE));
br = new BufferedReader(new InputStreamReader(is));
return br.readLine();
}
finally
{
if (br != null) br.close();
if (is != null) is.close();
}
}
private static final transient String ACTIVE_STATE = "active";
private static final transient String STARTING_STATE = "starting";
private static final transient String INSTALLED_STATE = "installed";
private static final transient String UNINSTALLED_STATE = "uninstalled";
private void writePersistentState() throws Exception
{
OutputStream os = null;
BufferedWriter bw = null;
try
{
os = BundleCache.getSecureAction()
.getFileOutputStream(new File(m_archiveRootDir, BUNDLE_STATE_FILE));
bw = new BufferedWriter(new OutputStreamWriter(os));
String s = null;
switch (m_persistentState)
{
case Bundle.ACTIVE:
s = ACTIVE_STATE;
break;
case Bundle.STARTING:
s = STARTING_STATE;
break;
case Bundle.UNINSTALLED:
s = UNINSTALLED_STATE;
break;
default:
s = INSTALLED_STATE;
break;
}
bw.write(s, 0, s.length());
}
catch (IOException ex)
{
m_logger.log(
Logger.LOG_ERROR,
getClass().getName() + ": Unable to record state - " + ex);
throw ex;
}
finally
{
if (bw != null) bw.close();
if (os != null) os.close();
}
}
private int readPersistentState() throws Exception
{
int state = Bundle.INSTALLED;
// Get bundle state file.
File stateFile = new File(m_archiveRootDir, BUNDLE_STATE_FILE);
// If the state file doesn't exist, then
// assume the bundle was installed.
if (BundleCache.getSecureAction().fileExists(stateFile))
{
// Read the bundle state.
InputStream is = null;
BufferedReader br = null;
try
{
is = BundleCache.getSecureAction()
.getFileInputStream(stateFile);
br = new BufferedReader(new InputStreamReader(is));
String s = br.readLine();
if ((s != null) && s.equals(ACTIVE_STATE))
{
state = Bundle.ACTIVE;
}
else if ((s != null) && s.equals(STARTING_STATE))
{
state = Bundle.STARTING;
}
else if ((s != null) && s.equals(UNINSTALLED_STATE))
{
state = Bundle.UNINSTALLED;
}
else
{
state = Bundle.INSTALLED;
}
}
catch (Exception ex)
{
state = Bundle.INSTALLED;
}
finally
{
if (br != null) br.close();
if (is != null) is.close();
}
}
return state;
}
private void writeStartLevel() throws Exception
{
OutputStream os = null;
BufferedWriter bw = null;
try
{
os = BundleCache.getSecureAction()
.getFileOutputStream(new File(m_archiveRootDir, BUNDLE_START_LEVEL_FILE));
bw = new BufferedWriter(new OutputStreamWriter(os));
String s = Integer.toString(m_startLevel);
bw.write(s, 0, s.length());
}
catch (IOException ex)
{
m_logger.log(
Logger.LOG_ERROR,
getClass().getName() + ": Unable to record start level - " + ex);
throw ex;
}
finally
{
if (bw != null) bw.close();
if (os != null) os.close();
}
}
private int readStartLevel() throws Exception
{
int level = -1;
// Get bundle start level file.
File levelFile = new File(m_archiveRootDir, BUNDLE_START_LEVEL_FILE);
// If the start level file doesn't exist, then
// return an error.
if (!BundleCache.getSecureAction().fileExists(levelFile))
{
level = -1;
}
else
{
// Read the bundle start level.
InputStream is = null;
BufferedReader br= null;
try
{
is = BundleCache.getSecureAction()
.getFileInputStream(levelFile);
br = new BufferedReader(new InputStreamReader(is));
level = Integer.parseInt(br.readLine());
}
finally
{
if (br != null) br.close();
if (is != null) is.close();
}
}
return level;
}
private void writeLastModified() throws Exception
{
OutputStream os = null;
BufferedWriter bw = null;
try
{
os = BundleCache.getSecureAction()
.getFileOutputStream(new File(m_archiveRootDir, BUNDLE_LASTMODIFIED_FILE));
bw = new BufferedWriter(new OutputStreamWriter(os));
String s = Long.toString(m_lastModified);
bw.write(s, 0, s.length());
}
catch (IOException ex)
{
m_logger.log(
Logger.LOG_ERROR,
getClass().getName() + ": Unable to record start level - " + ex);
throw ex;
}
finally
{
if (bw != null) bw.close();
if (os != null) os.close();
}
}
private long readLastModified() throws Exception
{
long last = 0;
InputStream is = null;
BufferedReader br = null;
try
{
is = BundleCache.getSecureAction()
.getFileInputStream(new File(m_archiveRootDir, BUNDLE_LASTMODIFIED_FILE));
br = new BufferedReader(new InputStreamReader(is));
last = Long.parseLong(br.readLine());
}
catch (Exception ex)
{
last = 0;
}
finally
{
if (br != null) br.close();
if (is != null) is.close();
}
return last;
}
private void writeRefreshCount() throws Exception
{
OutputStream os = null;
BufferedWriter bw = null;
try
{
os = BundleCache.getSecureAction()
.getFileOutputStream(new File(m_archiveRootDir, REFRESH_COUNTER_FILE));
bw = new BufferedWriter(new OutputStreamWriter(os));
String s = Long.toString(m_refreshCount);
bw.write(s, 0, s.length());
}
catch (IOException ex)
{
m_logger.log(
Logger.LOG_ERROR,
getClass().getName() + ": Unable to write refresh count: " + ex);
throw ex;
}
finally
{
if (bw != null) bw.close();
if (os != null) os.close();
}
}
private long readRefreshCount() throws Exception
{
long count = 0;
InputStream is = null;
BufferedReader br = null;
try
{
is = BundleCache.getSecureAction()
.getFileInputStream(new File(m_archiveRootDir, REFRESH_COUNTER_FILE));
br = new BufferedReader(new InputStreamReader(is));
count = Long.parseLong(br.readLine());
}
catch (Exception ex)
{
count = 0;
}
finally
{
if (br != null) br.close();
if (is != null) is.close();
}
return count;
}
}