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 }