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 }