001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *   http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    package org.apache.felix.framework.cache;
020    
021    import java.io.*;
022    import java.net.URLDecoder;
023    
024    import java.util.Map;
025    import org.apache.felix.framework.Logger;
026    import org.osgi.framework.Bundle;
027    
028    /**
029     * <p>
030     * This class is a logical abstraction for a bundle archive. This class,
031     * combined with <tt>BundleCache</tt> and concrete <tt>BundleRevision</tt>
032     * subclasses, implement the bundle cache for Felix. The bundle archive
033     * abstracts the actual bundle content into revisions and the revisions
034     * provide access to the actual bundle content. When a bundle is
035     * installed it has one revision associated with its content. Updating a
036     * bundle adds another revision for the updated content. Any number of
037     * revisions can be associated with a bundle archive. When the bundle
038     * (or framework) is refreshed, then all old revisions are purged and only
039     * the most recent revision is maintained.
040     * </p>
041     * <p>
042     * The content associated with a revision can come in many forms, such as
043     * a standard JAR file or an exploded bundle directory. The bundle archive
044     * is responsible for creating all revision instances during invocations
045     * of the <tt>revise()</tt> method call. Internally, it determines the
046     * concrete type of revision type by examining the location string as an
047     * URL. Currently, it supports standard JAR files, referenced JAR files,
048     * and referenced directories. Examples of each type of URL are, respectively:
049     * </p>
050     * <ul>
051     *   <li><tt>http://www.foo.com/bundle.jar</tt></li>
052     *   <li><tt>reference:file:/foo/bundle.jar</tt></li>
053     *   <li><tt>reference:file:/foo/bundle/</tt></li>
054     * </ul>
055     * <p>
056     * The "<tt>reference:</tt>" notation signifies that the resource should be
057     * used "in place", meaning that they will not be copied. For referenced JAR
058     * files, some resources may still be copied, such as embedded JAR files or
059     * native libraries, but for referenced exploded bundle directories, nothing
060     * will be copied. Currently, reference URLs can only refer to "file:" targets.
061     * </p>
062     * @see org.apache.felix.framework.cache.BundleCache
063     * @see org.apache.felix.framework.cache.BundleRevision
064    **/
065    public class BundleArchive
066    {
067        public static final transient String FILE_PROTOCOL = "file:";
068        public static final transient String REFERENCE_PROTOCOL = "reference:";
069        public static final transient String INPUTSTREAM_PROTOCOL = "inputstream:";
070    
071        private static final transient String BUNDLE_ID_FILE = "bundle.id";
072        private static final transient String BUNDLE_LOCATION_FILE = "bundle.location";
073        private static final transient String CURRENT_LOCATION_FILE = "current.location";
074        private static final transient String REVISION_LOCATION_FILE = "revision.location";
075        private static final transient String BUNDLE_STATE_FILE = "bundle.state";
076        private static final transient String BUNDLE_START_LEVEL_FILE = "bundle.startlevel";
077        private static final transient String REFRESH_COUNTER_FILE = "refresh.counter";
078        private static final transient String BUNDLE_LASTMODIFIED_FILE = "bundle.lastmodified";
079        private static final transient String REVISION_DIRECTORY = "version";
080        private static final transient String DATA_DIRECTORY = "data";
081        private static final transient String ACTIVE_STATE = "active";
082        private static final transient String STARTING_STATE = "starting";
083        private static final transient String INSTALLED_STATE = "installed";
084        private static final transient String UNINSTALLED_STATE = "uninstalled";
085    
086        private final Logger m_logger;
087        private final Map m_configMap;
088        private long m_id = -1;
089        private final File m_archiveRootDir;
090        private String m_originalLocation = null;
091        private String m_currentLocation = null;
092        private int m_persistentState = -1;
093        private int m_startLevel = -1;
094        private long m_lastModified = -1;
095        private BundleRevision[] m_revisions = null;
096    
097        private long m_refreshCount = -1;
098    
099        /**
100         * <p>
101         * This constructor is only used by the system bundle archive implementation
102         * because it is special an is not really an archive.
103         * </p>
104        **/
105        public BundleArchive()
106        {
107            m_logger = null;
108            m_configMap = null;
109            m_archiveRootDir = null;
110        }
111    
112        /**
113         * <p>
114         * This constructor is used for creating new archives when a bundle is
115         * installed into the framework. Each archive receives a logger, a root
116         * directory, its associated bundle identifier, the associated bundle
117         * location string, and an input stream from which to read the bundle
118         * content. The root directory is where any required state can be
119         * stored. The input stream may be null, in which case the location is
120         * used as an URL to the bundle content.
121         * </p>
122         * @param logger the logger to be used by the archive.
123         * @param archiveRootDir the archive root directory for storing state.
124         * @param id the bundle identifier associated with the archive.
125         * @param location the bundle location string associated with the archive.
126         * @param is input stream from which to read the bundle content.
127         * @throws Exception if any error occurs.
128        **/
129        public BundleArchive(Logger logger, Map configMap, File archiveRootDir, long id,
130            String location, InputStream is) throws Exception
131        {
132            m_logger = logger;
133            m_configMap = configMap;
134            m_archiveRootDir = archiveRootDir;
135            m_id = id;
136            if (m_id <= 0)
137            {
138                throw new IllegalArgumentException(
139                    "Bundle ID cannot be less than or equal to zero.");
140            }
141            m_originalLocation = location;
142    
143            // Save state.
144            initialize();
145    
146            // Add a revision for the content.
147            revise(m_originalLocation, is);
148        }
149    
150        /**
151         * <p>
152         * This constructor is called when an archive for a bundle is being
153         * reconstructed when the framework is restarted. Each archive receives
154         * a logger, a root directory, and its associated bundle identifier.
155         * The root directory is where any required state can be stored.
156         * </p>
157         * @param logger the logger to be used by the archive.
158         * @param archiveRootDir the archive root directory for storing state.
159         * @param configMap configMap for BundleArchive
160         * @throws Exception if any error occurs.
161        **/
162        public BundleArchive(Logger logger, Map configMap, File archiveRootDir)
163            throws Exception
164        {
165            m_logger = logger;
166            m_configMap = configMap;
167            m_archiveRootDir = archiveRootDir;
168    
169            // Add a revision for each one that already exists in the file
170            // system. The file system might contain more than one revision
171            // if the bundle was updated in a previous session, but the
172            // framework was not refreshed; this might happen if the framework
173            // did not exit cleanly. We must create the existing revisions so
174            // that they can be properly purged.
175            int revisionCount = 0;
176            while (true)
177            {
178                // Count the number of existing revision directories, which
179                // will be in a directory named like:
180                //     "${REVISION_DIRECTORY)${refresh-count}.${revision-count}"
181                File revisionRootDir = new File(m_archiveRootDir,
182                    REVISION_DIRECTORY + getRefreshCount() + "." + revisionCount);
183                if (!BundleCache.getSecureAction().fileExists(revisionRootDir))
184                {
185                    break;
186                }
187    
188                // Increment the revision count.
189                revisionCount++;
190            }
191    
192            // If there are multiple revisions in the file system, then create
193            // an array that is big enough to hold all revisions minus one; the
194            // call below to revise() will add the most recent revision. NOTE: We
195            // do not actually need to add a real revision object for the older
196            // revisions since they will be purged immediately on framework startup.
197            if (revisionCount > 1)
198            {
199                m_revisions = new BundleRevision[revisionCount - 1];
200            }
201    
202            // Add the revision object for the most recent revision. We first try
203            // to read the location from the current revision - if that fails we
204            // likely have an old bundle cache and read the location the old way.
205            // The next revision will update the bundle cache.
206            revise(getRevisionLocation(revisionCount - 1), null);
207        }
208    
209        /**
210         * <p>
211         * Returns the bundle identifier associated with this archive.
212         * </p>
213         * @return the bundle identifier associated with this archive.
214         * @throws Exception if any error occurs.
215        **/
216        public synchronized long getId() throws Exception
217        {
218            if (m_id > 0)
219            {
220                return m_id;
221            }
222    
223            // Read bundle location.
224            InputStream is = null;
225            BufferedReader br = null;
226            try
227            {
228                is = BundleCache.getSecureAction()
229                    .getFileInputStream(new File(m_archiveRootDir, BUNDLE_ID_FILE));
230                br = new BufferedReader(new InputStreamReader(is));
231                m_id = Long.parseLong(br.readLine());
232            }
233            catch (FileNotFoundException ex)
234            {
235                // HACK: Get the bundle identifier from the archive root directory
236                // name, which is of the form "bundle<id>" where <id> is the bundle
237                // identifier numbers. This is a hack to deal with old archives that
238                // did not save their bundle identifier, but instead had it passed
239                // into them. Eventually, this can be removed.
240                m_id = Long.parseLong(
241                    m_archiveRootDir.getName().substring(
242                        BundleCache.BUNDLE_DIR_PREFIX.length()));
243            }
244            finally
245            {
246                if (br != null) br.close();
247                if (is != null) is.close();
248            }
249    
250            return m_id;
251        }
252    
253        /**
254         * <p>
255         * Returns the location string associated with this archive.
256         * </p>
257         * @return the location string associated with this archive.
258         * @throws Exception if any error occurs.
259        **/
260        public synchronized String getLocation() throws Exception
261        {
262            if (m_originalLocation != null)
263            {
264                return m_originalLocation;
265            }
266    
267            // Read bundle location.
268            InputStream is = null;
269            BufferedReader br = null;
270            try
271            {
272                is = BundleCache.getSecureAction()
273                    .getFileInputStream(new File(m_archiveRootDir, BUNDLE_LOCATION_FILE));
274                br = new BufferedReader(new InputStreamReader(is));
275                m_originalLocation = br.readLine();
276                return m_originalLocation;
277            }
278            finally
279            {
280                if (br != null) br.close();
281                if (is != null) is.close();
282            }
283        }
284    
285        /**
286         * <p>
287         * Returns the persistent state of this archive. The value returned is
288         * one of the following: <tt>Bundle.INSTALLED</tt>, <tt>Bundle.ACTIVE</tt>,
289         * or <tt>Bundle.UNINSTALLED</tt>.
290         * </p>
291         * @return the persistent state of this archive.
292         * @throws Exception if any error occurs.
293        **/
294        public synchronized int getPersistentState() throws Exception
295        {
296            if (m_persistentState >= 0)
297            {
298                return m_persistentState;
299            }
300    
301            // Get bundle state file.
302            File stateFile = new File(m_archiveRootDir, BUNDLE_STATE_FILE);
303    
304            // If the state file doesn't exist, then
305            // assume the bundle was installed.
306            if (!BundleCache.getSecureAction().fileExists(stateFile))
307            {
308                return Bundle.INSTALLED;
309            }
310    
311            // Read the bundle state.
312            InputStream is = null;
313            BufferedReader br = null;
314            try
315            {
316                is = BundleCache.getSecureAction()
317                    .getFileInputStream(stateFile);
318                br = new BufferedReader(new InputStreamReader(is));
319                String s = br.readLine();
320                if ((s != null) && s.equals(ACTIVE_STATE))
321                {
322                    m_persistentState = Bundle.ACTIVE;
323                }
324                else if ((s != null) && s.equals(STARTING_STATE))
325                {
326                    m_persistentState = Bundle.STARTING;
327                }
328                else if ((s != null) && s.equals(UNINSTALLED_STATE))
329                {
330                    m_persistentState = Bundle.UNINSTALLED;
331                }
332                else
333                {
334                    m_persistentState = Bundle.INSTALLED;
335                }
336                return m_persistentState;
337            }
338            finally
339            {
340                if (br != null) br.close();
341                if (is != null) is.close();
342            }
343        }
344    
345        /**
346         * <p>
347         * Sets the persistent state of this archive. The value is
348         * one of the following: <tt>Bundle.INSTALLED</tt>, <tt>Bundle.ACTIVE</tt>,
349         * or <tt>Bundle.UNINSTALLED</tt>.
350         * </p>
351         * @param state the persistent state value to set for this archive.
352         * @throws Exception if any error occurs.
353        **/
354        public synchronized void setPersistentState(int state) throws Exception
355        {
356            // Write the bundle state.
357            OutputStream os = null;
358            BufferedWriter bw = null;
359            try
360            {
361                os = BundleCache.getSecureAction()
362                    .getFileOutputStream(new File(m_archiveRootDir, BUNDLE_STATE_FILE));
363                bw = new BufferedWriter(new OutputStreamWriter(os));
364                String s = null;
365                switch (state)
366                {
367                    case Bundle.ACTIVE:
368                        s = ACTIVE_STATE;
369                        break;
370                    case Bundle.STARTING:
371                        s = STARTING_STATE;
372                        break;
373                    case Bundle.UNINSTALLED:
374                        s = UNINSTALLED_STATE;
375                        break;
376                    default:
377                        s = INSTALLED_STATE;
378                        break;
379                }
380                bw.write(s, 0, s.length());
381                m_persistentState = state;
382            }
383            catch (IOException ex)
384            {
385                m_logger.log(
386                    Logger.LOG_ERROR,
387                    getClass().getName() + ": Unable to record state - " + ex);
388                throw ex;
389            }
390            finally
391            {
392                if (bw != null) bw.close();
393                if (os != null) os.close();
394            }
395        }
396    
397        /**
398         * <p>
399         * Returns the start level of this archive.
400         * </p>
401         * @return the start level of this archive.
402         * @throws Exception if any error occurs.
403        **/
404        public synchronized int getStartLevel() throws Exception
405        {
406            if (m_startLevel >= 0)
407            {
408                return m_startLevel;
409            }
410    
411            // Get bundle start level file.
412            File levelFile = new File(m_archiveRootDir, BUNDLE_START_LEVEL_FILE);
413    
414            // If the start level file doesn't exist, then
415            // return an error.
416            if (!BundleCache.getSecureAction().fileExists(levelFile))
417            {
418                return -1;
419            }
420    
421            // Read the bundle start level.
422            InputStream is = null;
423            BufferedReader br= null;
424            try
425            {
426                is = BundleCache.getSecureAction()
427                    .getFileInputStream(levelFile);
428                br = new BufferedReader(new InputStreamReader(is));
429                m_startLevel = Integer.parseInt(br.readLine());
430                return m_startLevel;
431            }
432            finally
433            {
434                if (br != null) br.close();
435                if (is != null) is.close();
436            }
437        }
438    
439        /**
440         * <p>
441         * Sets the the start level of this archive this archive.
442         * </p>
443         * @param level the start level to set for this archive.
444         * @throws Exception if any error occurs.
445        **/
446        public synchronized void setStartLevel(int level) throws Exception
447        {
448            // Write the bundle start level.
449            OutputStream os = null;
450            BufferedWriter bw = null;
451            try
452            {
453                os = BundleCache.getSecureAction()
454                    .getFileOutputStream(new File(m_archiveRootDir, BUNDLE_START_LEVEL_FILE));
455                bw = new BufferedWriter(new OutputStreamWriter(os));
456                String s = Integer.toString(level);
457                bw.write(s, 0, s.length());
458                m_startLevel = level;
459            }
460            catch (IOException ex)
461            {
462                m_logger.log(
463                    Logger.LOG_ERROR,
464                    getClass().getName() + ": Unable to record start level - " + ex);
465                throw ex;
466            }
467            finally
468            {
469                if (bw != null) bw.close();
470                if (os != null) os.close();
471            }
472        }
473    
474        /**
475         * <p>
476         * Returns the last modification time of this archive.
477         * </p>
478         * @return the last modification time of this archive.
479         * @throws Exception if any error occurs.
480        **/
481        public synchronized long getLastModified() throws Exception
482        {
483            if (m_lastModified >= 0)
484            {
485                return m_lastModified;
486            }
487    
488            // Get bundle last modification time file.
489            File lastModFile = new File(m_archiveRootDir, BUNDLE_LASTMODIFIED_FILE);
490    
491            // If the last modification file doesn't exist, then
492            // return an error.
493            if (!BundleCache.getSecureAction().fileExists(lastModFile))
494            {
495                return 0;
496            }
497    
498            // Read the bundle start level.
499            InputStream is = null;
500            BufferedReader br= null;
501            try
502            {
503                is = BundleCache.getSecureAction().getFileInputStream(lastModFile);
504                br = new BufferedReader(new InputStreamReader(is));
505                m_lastModified = Long.parseLong(br.readLine());
506                return m_lastModified;
507            }
508            finally
509            {
510                if (br != null) br.close();
511                if (is != null) is.close();
512            }
513        }
514    
515        /**
516         * <p>
517         * Sets the the last modification time of this archive.
518         * </p>
519         * @param lastModified The time of the last modification to set for
520         *      this archive. According to the OSGi specification this time is
521         *      set each time a bundle is installed, updated or uninstalled.
522         *
523         * @throws Exception if any error occurs.
524        **/
525        public synchronized void setLastModified(long lastModified) throws Exception
526        {
527            // Write the bundle last modification time.
528            OutputStream os = null;
529            BufferedWriter bw = null;
530            try
531            {
532                os = BundleCache.getSecureAction()
533                    .getFileOutputStream(new File(m_archiveRootDir, BUNDLE_LASTMODIFIED_FILE));
534                bw = new BufferedWriter(new OutputStreamWriter(os));
535                String s = Long.toString(lastModified);
536                bw.write(s, 0, s.length());
537                m_lastModified = lastModified;
538            }
539            catch (IOException ex)
540            {
541                m_logger.log(
542                    Logger.LOG_ERROR,
543                    getClass().getName() + ": Unable to record last modification time - " + ex);
544                throw ex;
545            }
546            finally
547            {
548                if (bw != null) bw.close();
549                if (os != null) os.close();
550            }
551        }
552    
553        /**
554         * <p>
555         * Returns a <tt>File</tt> object corresponding to the data file
556         * of the relative path of the specified string.
557         * </p>
558         * @return a <tt>File</tt> object corresponding to the specified file name.
559         * @throws Exception if any error occurs.
560        **/
561        public synchronized File getDataFile(String fileName) throws Exception
562        {
563            // Do some sanity checking.
564            if ((fileName.length() > 0) && (fileName.charAt(0) == File.separatorChar))
565                throw new IllegalArgumentException(
566                    "The data file path must be relative, not absolute.");
567            else if (fileName.indexOf("..") >= 0)
568                throw new IllegalArgumentException(
569                    "The data file path cannot contain a reference to the \"..\" directory.");
570    
571            // Get bundle data directory.
572            File dataDir = new File(m_archiveRootDir, DATA_DIRECTORY);
573            // Create the data directory if necessary.
574            if (!BundleCache.getSecureAction().fileExists(dataDir))
575            {
576                if (!BundleCache.getSecureAction().mkdir(dataDir))
577                {
578                    throw new IOException("Unable to create bundle data directory.");
579                }
580            }
581    
582            // Return the data file.
583            return new File(dataDir, fileName);
584        }
585    
586        /**
587         * <p>
588         * Returns the number of revisions available for this archive.
589         * </p>
590         * @return tthe number of revisions available for this archive.
591        **/
592        public synchronized int getRevisionCount()
593        {
594            return (m_revisions == null) ? 0 : m_revisions.length;
595        }
596    
597        /**
598         * <p>
599         * Returns the revision object for the specified revision.
600         * </p>
601         * @return the revision object for the specified revision.
602        **/
603        public synchronized BundleRevision getRevision(int i)
604        {
605            if ((i >= 0) && (i < getRevisionCount()))
606            {
607                return m_revisions[i];
608            }
609            return null;
610        }
611    
612        /**
613         * <p>
614         * This method adds a revision to the archive. The revision is created
615         * based on the specified location and/or input stream.
616         * </p>
617         * @param location the location string associated with the revision.
618         * @throws Exception if any error occurs.
619        **/
620        public synchronized void revise(String location, InputStream is)
621            throws Exception
622        {
623            // If we have an input stream, then we have to use it
624            // no matter what the update location is, so just ignore
625            // the update location and set the location to be input
626            // stream.
627            if (is != null)
628            {
629                location = "inputstream:";
630            }
631            BundleRevision revision = createRevisionFromLocation(location, is);
632            if (revision == null)
633            {
634                throw new Exception("Unable to revise archive.");
635            }
636    
637            setRevisionLocation(location, (m_revisions == null) ? 0 : m_revisions.length);
638    
639            // Add new revision to revision array.
640            if (m_revisions == null)
641            {
642                m_revisions = new BundleRevision[] { revision };
643            }
644            else
645            {
646                BundleRevision[] tmp = new BundleRevision[m_revisions.length + 1];
647                System.arraycopy(m_revisions, 0, tmp, 0, m_revisions.length);
648                tmp[m_revisions.length] = revision;
649                m_revisions = tmp;
650            }
651        }
652    
653        /**
654         * <p>
655         * This method undoes the previous revision to the archive; this method will
656         * remove the latest revision from the archive. This method is only called
657         * when there are problems during an update after the revision has been
658         * created, such as errors in the update bundle's manifest. This method
659         * can only be called if there is more than one revision, otherwise there
660         * is nothing to undo.
661         * </p>
662         * @return true if the undo was a success false if there is no previous revision
663         * @throws Exception if any error occurs.
664         */
665        public synchronized boolean rollbackRevise() throws Exception
666        {
667            // Can only undo the revision if there is more than one.
668            if (getRevisionCount() <= 1)
669            {
670                return false;
671            }
672    
673            String location = getRevisionLocation(m_revisions.length - 2);
674    
675            try
676            {
677                m_revisions[m_revisions.length - 1].close();
678            }
679            catch(Exception ex)
680            {
681               m_logger.log(Logger.LOG_ERROR, getClass().getName() +
682                   ": Unable to dispose latest revision", ex);
683            }
684    
685            File revisionDir = new File(m_archiveRootDir, REVISION_DIRECTORY +
686                getRefreshCount() + "." + (m_revisions.length - 1));
687    
688            if (BundleCache.getSecureAction().fileExists(revisionDir))
689            {
690                BundleCache.deleteDirectoryTree(revisionDir);
691            }
692    
693            BundleRevision[] tmp = new BundleRevision[m_revisions.length - 1];
694            System.arraycopy(m_revisions, 0, tmp, 0, m_revisions.length - 1);
695            m_revisions = tmp;
696    
697            return true;
698        }
699    
700        private synchronized String getRevisionLocation(int revision) throws Exception
701        {
702            InputStream is = null;
703            BufferedReader br = null;
704            try
705            {
706                is = BundleCache.getSecureAction().getFileInputStream(new File(
707                    new File(m_archiveRootDir, REVISION_DIRECTORY +
708                    getRefreshCount() + "." + revision), REVISION_LOCATION_FILE));
709    
710                br = new BufferedReader(new InputStreamReader(is));
711                return br.readLine();
712            }
713            finally
714            {
715                if (br != null) br.close();
716                if (is != null) is.close();
717            }
718        }
719    
720        private synchronized void setRevisionLocation(String location, int revision) throws Exception
721        {
722            // Save current revision location.
723            OutputStream os = null;
724            BufferedWriter bw = null;
725            try
726            {
727                os = BundleCache.getSecureAction()
728                    .getFileOutputStream(new File(
729                        new File(m_archiveRootDir, REVISION_DIRECTORY +
730                        getRefreshCount() + "." + revision), REVISION_LOCATION_FILE));
731                bw = new BufferedWriter(new OutputStreamWriter(os));
732                bw.write(location, 0, location.length());
733            }
734            finally
735            {
736                if (bw != null) bw.close();
737                if (os != null) os.close();
738            }
739        }
740    
741        public synchronized void close()
742        {
743            // Get the current revision count.
744            int count = getRevisionCount();
745            for (int i = 0; i < count; i++)
746            {
747                // Dispose of the revision, but this might be null in certain
748                // circumstances, such as if this bundle archive was created
749                // for an existing bundle that was updated, but not refreshed
750                // due to a system crash; see the constructor code for details.
751                if (m_revisions[i] != null)
752                {
753                    try
754                    {
755                        m_revisions[i].close();
756                    }
757                    catch (Exception ex)
758                    {
759                        m_logger.log(
760                            Logger.LOG_ERROR,
761                                "Unable to close revision - "
762                                + m_revisions[i].getRevisionRootDir(), ex);
763                    }
764                }
765            }
766        }
767    
768        /**
769         * <p>
770         * This method closes any revisions and deletes the bundle archive directory.
771         * </p>
772         * @throws Exception if any error occurs.
773        **/
774        public synchronized void closeAndDelete()
775        {
776            // Close the revisions and delete the archive directory.
777            close();
778            if (!BundleCache.deleteDirectoryTree(m_archiveRootDir))
779            {
780                m_logger.log(
781                    Logger.LOG_ERROR,
782                    "Unable to delete archive directory - " + m_archiveRootDir);
783            }
784        }
785    
786        /**
787         * <p>
788         * This method removes all old revisions associated with the archive
789         * and keeps only the current revision.
790         * </p>
791         * @throws Exception if any error occurs.
792        **/
793        public synchronized void purge() throws Exception
794        {
795            // Close the revisions and then delete all but the current revision.
796            // We don't delete it the current revision, because we want to rename it
797            // to the new refresh level.
798            close();
799            long refreshCount = getRefreshCount();
800            int count = getRevisionCount();
801            File revisionDir = null;
802            for (int i = 0; i < count - 1; i++)
803            {
804                revisionDir = new File(m_archiveRootDir, REVISION_DIRECTORY + refreshCount + "." + i);
805                if (BundleCache.getSecureAction().fileExists(revisionDir))
806                {
807                    BundleCache.deleteDirectoryTree(revisionDir);
808                }
809            }
810    
811            // Save the current revision location for use later when
812            // we recreate the revision.
813            String location = getRevisionLocation(count -1);
814    
815            // Increment the refresh count.
816            setRefreshCount(refreshCount + 1);
817    
818            // Rename the current revision directory to be the zero revision
819            // of the new refresh level.
820            File currentDir = new File(m_archiveRootDir, REVISION_DIRECTORY + (refreshCount + 1) + ".0");
821            revisionDir = new File(m_archiveRootDir, REVISION_DIRECTORY + refreshCount + "." + (count - 1));
822            BundleCache.getSecureAction().renameFile(revisionDir, currentDir);
823    
824            // Null the revision array since they are all invalid now.
825            m_revisions = null;
826            // Finally, recreate the revision for the current location.
827            BundleRevision revision = createRevisionFromLocation(location, null);
828            // Create new revision array.
829            m_revisions = new BundleRevision[] { revision };
830        }
831    
832        /**
833         * <p>
834         * Initializes the bundle archive object by creating the archive
835         * root directory and saving the initial state.
836         * </p>
837         * @throws Exception if any error occurs.
838        **/
839        private void initialize() throws Exception
840        {
841            OutputStream os = null;
842            BufferedWriter bw = null;
843    
844            try
845            {
846                // If the archive directory exists, then we don't
847                // need to initialize since it has already been done.
848                if (BundleCache.getSecureAction().fileExists(m_archiveRootDir))
849                {
850                    return;
851                }
852    
853                // Create archive directory, if it does not exist.
854                if (!BundleCache.getSecureAction().mkdir(m_archiveRootDir))
855                {
856                    m_logger.log(
857                        Logger.LOG_ERROR,
858                        getClass().getName() + ": Unable to create archive directory.");
859                    throw new IOException("Unable to create archive directory.");
860                }
861    
862                // Save id.
863                os = BundleCache.getSecureAction()
864                    .getFileOutputStream(new File(m_archiveRootDir, BUNDLE_ID_FILE));
865                bw = new BufferedWriter(new OutputStreamWriter(os));
866                bw.write(Long.toString(m_id), 0, Long.toString(m_id).length());
867                bw.close();
868                os.close();
869    
870                // Save location string.
871                os = BundleCache.getSecureAction()
872                    .getFileOutputStream(new File(m_archiveRootDir, BUNDLE_LOCATION_FILE));
873                bw = new BufferedWriter(new OutputStreamWriter(os));
874                bw.write(m_originalLocation, 0, m_originalLocation.length());
875            }
876            finally
877            {
878                if (bw != null) bw.close();
879                if (os != null) os.close();
880            }
881        }
882    
883        /**
884         * <p>
885         * Returns the current location associated with the bundle archive,
886         * which is the last location from which the bundle was updated. It is
887         * necessary to keep track of this so it is possible to determine what
888         * kind of revision needs to be created when recreating revisions when
889         * the framework restarts.
890         * </p>
891         * @return the last update location.
892         * @throws Exception if any error occurs.
893        **/
894        private String getCurrentLocation() throws Exception
895        {
896            if (m_currentLocation != null)
897            {
898                return m_currentLocation;
899            }
900    
901            // Read current location.
902            InputStream is = null;
903            BufferedReader br = null;
904            try
905            {
906                is = BundleCache.getSecureAction()
907                    .getFileInputStream(new File(m_archiveRootDir, CURRENT_LOCATION_FILE));
908                br = new BufferedReader(new InputStreamReader(is));
909                m_currentLocation = br.readLine();
910                return m_currentLocation;
911            }
912            catch (FileNotFoundException ex)
913            {
914                return getLocation();
915            }
916            finally
917            {
918                if (br != null) br.close();
919                if (is != null) is.close();
920            }
921        }
922    
923        /**
924         * <p>
925         * Set the current location associated with the bundle archive,
926         * which is the last location from which the bundle was updated. It is
927         * necessary to keep track of this so it is possible to determine what
928         * kind of revision needs to be created when recreating revisions when
929         * the framework restarts.
930         * </p>
931         * @throws Exception if any error occurs.
932        **/
933        private void setCurrentLocation(String location) throws Exception
934        {
935            // Save current location.
936            OutputStream os = null;
937            BufferedWriter bw = null;
938            try
939            {
940                os = BundleCache.getSecureAction()
941                    .getFileOutputStream(new File(m_archiveRootDir, CURRENT_LOCATION_FILE));
942                bw = new BufferedWriter(new OutputStreamWriter(os));
943                bw.write(location, 0, location.length());
944                m_currentLocation = location;
945            }
946            finally
947            {
948                if (bw != null) bw.close();
949                if (os != null) os.close();
950            }
951        }
952    
953        /**
954         * <p>
955         * Creates a revision based on the location string and/or input stream.
956         * </p>
957         * @return the location string associated with this archive.
958        **/
959        private BundleRevision createRevisionFromLocation(String location, InputStream is)
960            throws Exception
961        {
962            // The revision directory is named using the refresh count and
963            // the revision count. The revision count is obvious, but the
964            // refresh count is less obvious. This is necessary due to how
965            // native libraries are handled in Java; needless to say, every
966            // time a bundle is refreshed we must change the name of its
967            // native libraries so that we can reload them. Thus, we use the
968            // refresh counter as a way to change the name of the revision
969            // directory to give native libraries new absolute names.
970            File revisionRootDir = new File(m_archiveRootDir,
971                REVISION_DIRECTORY + getRefreshCount() + "." + getRevisionCount());
972    
973            BundleRevision result = null;
974    
975            try
976            {
977                // Check if the location string represents a reference URL.
978                if ((location != null) && location.startsWith(REFERENCE_PROTOCOL))
979                {
980                    // Reference URLs only support the file protocol.
981                    location = location.substring(REFERENCE_PROTOCOL.length());
982                    if (!location.startsWith(FILE_PROTOCOL))
983                    {
984                        throw new IOException("Reference URLs can only be files: " + location);
985                    }
986    
987                    // Decode any URL escaped sequences.
988                    location = decode(location);
989    
990                    // Make sure the referenced file exists.
991                    File file = new File(location.substring(FILE_PROTOCOL.length()));
992                    if (!BundleCache.getSecureAction().fileExists(file))
993                    {
994                        throw new IOException("Referenced file does not exist: " + file);
995                    }
996    
997                    // If the referenced file is a directory, then create a directory
998                    // revision; otherwise, create a JAR revision with the reference
999                    // flag set to true.
1000                    if (BundleCache.getSecureAction().isFileDirectory(file))
1001                    {
1002                        result = new DirectoryRevision(m_logger, m_configMap,
1003                            revisionRootDir, location);
1004                    }
1005                    else
1006                    {
1007                        result = new JarRevision(m_logger, m_configMap, revisionRootDir,
1008                            location, true);
1009                    }
1010                }
1011                else if (location.startsWith(INPUTSTREAM_PROTOCOL))
1012                {
1013                    // Assume all input streams point to JAR files.
1014                    result = new JarRevision(m_logger, m_configMap, revisionRootDir,
1015                        location, false, is);
1016                }
1017                else
1018                {
1019                    // Anything else is assumed to be a URL to a JAR file.
1020                    result = new JarRevision(m_logger, m_configMap, revisionRootDir,
1021                        location, false);
1022                }
1023            }
1024            catch (Exception ex)
1025            {
1026                if (BundleCache.getSecureAction().fileExists(revisionRootDir))
1027                {
1028                    if (!BundleCache.deleteDirectoryTree(revisionRootDir))
1029                    {
1030                        m_logger.log(
1031                            Logger.LOG_ERROR,
1032                            getClass().getName()
1033                                + ": Unable to delete revision directory - "
1034                                + revisionRootDir);
1035                    }
1036                }
1037                throw ex;
1038            }
1039    
1040            return result;
1041        }
1042    
1043        // Method from Harmony java.net.URIEncoderDecoder (luni subproject)
1044        // used by URI to decode uri components.
1045        private static String decode(String s) throws UnsupportedEncodingException
1046        {
1047            StringBuffer result = new StringBuffer();
1048            ByteArrayOutputStream out = new ByteArrayOutputStream();
1049            for (int i = 0; i < s.length(); )
1050            {
1051                char c = s.charAt(i);
1052                if (c == '%')
1053                {
1054                    out.reset();
1055                    do
1056                    {
1057                        if (i + 2 >= s.length())
1058                        {
1059                            throw new IllegalArgumentException(
1060                                "Incomplete % sequence at: " + i);
1061                        }
1062                        int d1 = Character.digit(s.charAt(i + 1), 16);
1063                        int d2 = Character.digit(s.charAt(i + 2), 16);
1064                        if ((d1 == -1) || (d2 == -1))
1065                        {
1066                            throw new IllegalArgumentException(
1067                                "Invalid % sequence ("
1068                                + s.substring(i, i + 3)
1069                                + ") at: " + String.valueOf(i));
1070                        }
1071                        out.write((byte) ((d1 << 4) + d2));
1072                        i += 3;
1073                    }
1074                    while ((i < s.length()) && (s.charAt(i) == '%'));
1075                    result.append(out.toString("UTF8"));
1076                    continue;
1077                }
1078                result.append(c);
1079                i++;
1080            }
1081            return result.toString();
1082        }
1083    
1084        /**
1085         * This utility method is used to retrieve the current refresh
1086         * counter value for the bundle. This value is used when generating
1087         * the bundle revision directory name where native libraries are extracted.
1088         * This is necessary because Sun's JVM requires a one-to-one mapping
1089         * between native libraries and class loaders where the native library
1090         * is uniquely identified by its absolute path in the file system. This
1091         * constraint creates a problem when a bundle is refreshed, because it
1092         * gets a new class loader. Using the refresh counter to generate the name
1093         * of the bundle revision directory resolves this problem because each time
1094         * bundle is refresh, the native library will have a unique name.
1095         * As a result of the unique name, the JVM will then reload the
1096         * native library without a problem.
1097        **/
1098        private long getRefreshCount() throws Exception
1099        {
1100            // If we have already read the refresh counter file,
1101            // then just return the result.
1102            if (m_refreshCount >= 0)
1103            {
1104                return m_refreshCount;
1105            }
1106    
1107            // Get refresh counter file.
1108            File counterFile = new File(m_archiveRootDir, REFRESH_COUNTER_FILE);
1109    
1110            // If the refresh counter file doesn't exist, then
1111            // assume the counter is at zero.
1112            if (!BundleCache.getSecureAction().fileExists(counterFile))
1113            {
1114                return 0;
1115            }
1116    
1117            // Read the bundle refresh counter.
1118            InputStream is = null;
1119            BufferedReader br = null;
1120            try
1121            {
1122                is = BundleCache.getSecureAction()
1123                    .getFileInputStream(counterFile);
1124                br = new BufferedReader(new InputStreamReader(is));
1125                long counter = Long.parseLong(br.readLine());
1126                return counter;
1127            }
1128            finally
1129            {
1130                if (br != null) br.close();
1131                if (is != null) is.close();
1132            }
1133        }
1134    
1135        /**
1136         * This utility method is used to retrieve the current refresh
1137         * counter value for the bundle. This value is used when generating
1138         * the bundle revision directory name where native libraries are extracted.
1139         * This is necessary because Sun's JVM requires a one-to-one mapping
1140         * between native libraries and class loaders where the native library
1141         * is uniquely identified by its absolute path in the file system. This
1142         * constraint creates a problem when a bundle is refreshed, because it
1143         * gets a new class loader. Using the refresh counter to generate the name
1144         * of the bundle revision directory resolves this problem because each time
1145         * bundle is refresh, the native library will have a unique name.
1146         * As a result of the unique name, the JVM will then reload the
1147         * native library without a problem.
1148        **/
1149        private void setRefreshCount(long counter)
1150            throws Exception
1151        {
1152            // Get refresh counter file.
1153            File counterFile = new File(m_archiveRootDir, REFRESH_COUNTER_FILE);
1154    
1155            // Write the refresh counter.
1156            OutputStream os = null;
1157            BufferedWriter bw = null;
1158            try
1159            {
1160                os = BundleCache.getSecureAction()
1161                    .getFileOutputStream(counterFile);
1162                bw = new BufferedWriter(new OutputStreamWriter(os));
1163                String s = Long.toString(counter);
1164                bw.write(s, 0, s.length());
1165                m_refreshCount = counter;
1166            }
1167            catch (IOException ex)
1168            {
1169                m_logger.log(
1170                    Logger.LOG_ERROR,
1171                    getClass().getName() + ": Unable to write refresh counter: " + ex);
1172                throw ex;
1173            }
1174            finally
1175            {
1176                if (bw != null) bw.close();
1177                if (os != null) os.close();
1178            }
1179        }
1180    }