| /* |
| * 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 |
| **/ |
| 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 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; |
| |
| // 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; |
| |
| 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 |
| { |
| 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 |
| { |
| 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 |
| { |
| 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; |
| writeBundleInfo(); |
| } |
| } |
| |
| /** |
| * <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 |
| { |
| 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; |
| writeBundleInfo(); |
| } |
| } |
| |
| /** |
| * <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 |
| { |
| 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; |
| writeBundleInfo(); |
| } |
| } |
| |
| /** |
| * 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 |
| { |
| 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; |
| writeBundleInfo(); |
| } |
| } |
| |
| /** |
| * <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 File getDataFile(String fileName) throws Exception |
| { |
| // Get bundle data directory. |
| File dataDir = new File(m_archiveRootDir, DATA_DIRECTORY); |
| |
| // Create the data directory if necessary. |
| if (!BundleCache.getSecureAction().fileExists(dataDir) && !BundleCache.getSecureAction().mkdirs(dataDir) && !BundleCache.getSecureAction().fileExists(dataDir)) |
| { |
| throw new IOException("Unable to create bundle data directory."); |
| } |
| |
| File dataFile = new File(dataDir, fileName); |
| |
| String dataFilePath = BundleCache.getSecureAction().getCanonicalPath(dataFile); |
| String dataDirPath = BundleCache.getSecureAction().getCanonicalPath(dataDir); |
| if (!dataFilePath.equals(dataDirPath) && !dataFilePath.startsWith(dataDirPath + File.separatorChar)) |
| { |
| throw new IllegalArgumentException("The data file must be inside the data dir."); |
| } |
| |
| // Return the data file. |
| return dataFile; |
| } |
| |
| /** |
| * <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. |
| Map<String, Object> headers = getCurrentRevision().getManifestHeader(); |
| |
| boolean hasNativeLibs = headers != null && 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."); |
| } |
| |
| writeBundleInfo(); |
| } |
| 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 |
| { |
| StringBuilder result = new StringBuilder(); |
| 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()); |
| } |
| 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(); |
| } |
| } |
| } |