001/* 002 * IzPack - Copyright 2001-2005 Julien Ponge, All Rights Reserved. 003 * 004 * http://www.izforge.com/izpack/ 005 * http://developer.berlios.de/projects/izpack/ 006 * 007 * Copyright 2002 Elmar Grom 008 * 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 */ 021 022package com.izforge.izpack.util; 023 024import java.io.File; 025import java.io.FileNotFoundException; 026import java.io.FileOutputStream; 027import java.io.InputStream; 028import java.io.OutputStream; 029import java.net.URL; 030import java.security.CodeSource; 031import java.security.ProtectionDomain; 032import java.text.CharacterIterator; 033import java.text.StringCharacterIterator; 034import java.util.Vector; 035 036/*---------------------------------------------------------------------------*/ 037/** 038 * This class handles loading of native libraries. There must only be one instance of 039 * <code>Librarian</code> per Java runtime, therefore this class is implemented as a 'Singleton'. 040 * <br> 041 * <br> 042 * <code>Librarian</code> is capable of loading native libraries from a variety of different 043 * source locations. However, you should place your library files in the 'native' directory. The 044 * primary reason for supporting different source locations is to facilitate testing in a 045 * development environment, without the need to actually packing the application into a *.jar file. 046 * 047 * @version 1.0 / 1/30/02 048 * @author Elmar Grom 049 */ 050/*---------------------------------------------------------------------------*/ 051public class Librarian implements CleanupClient 052{ 053 054 // ------------------------------------------------------------------------ 055 // Constant Definitions 056 // ------------------------------------------------------------------------ 057 058 /** Used to identify jar URL protocols */ 059 private static final String JAR_PROTOCOL = "jar"; 060 061 /** Used to identify file URL protocols */ 062 private static final String FILE_PROTOCOL = "file"; 063 064 /** 065 * The key used to retrieve the location of temporary files form the system properties. 066 */ 067 private static final String TEMP_LOCATION_KEY = "java.io.tmpdir"; 068 069 /** 070 * The extension appended to the client name when searching for it as a resource. Since the 071 * client is an object, the extension should always be '.class' 072 */ 073 private static final String CLIENT_EXTENSION = ".class"; 074 075 /** The default directory for native library files. */ 076 private static final String NATIVE = "native"; 077 078 /** The block size used for reading and writing data, 4k. */ 079 private static final int BLOCK_SIZE = 4096; 080 081 // ------------------------------------------------------------------------ 082 // Variable Declarations 083 // ------------------------------------------------------------------------ 084 085 /** 086 * The reference to the single instance of <code>Librarian</code>. Used in static methods in 087 * place of <code>this</code>. 088 */ 089 private static Librarian me = null; 090 091 /** 092 * A list that is used to track all libraries that have been loaded. This list is used to ensure 093 * that each library is loaded only once. 094 */ 095 private Vector trackList = new Vector(); 096 097 /** 098 * A list of references to clients that use libraries that were extracted from a *.jar file. 099 * This is needed because the clients need to be called for freeing their libraries. 100 */ 101 private Vector clients = new Vector(); 102 103 /** 104 * A list of library names as they appear in the temporary directory. This is needed to free 105 * each library through the client. The index of each name corresponds to the index of the 106 * respective client in the <code>clients</code> list. 107 */ 108 private Vector libraryNames = new Vector(); 109 110 /** 111 * A list of fully qualified library names. This is needed to delete the temporary library files 112 * after use. The index of each name corresponds to the index of the respective client in the 113 * <code>clients</code> list. 114 */ 115 private Vector temporaryFileNames = new Vector(); 116 117 /** The extension to use for native libraries. */ 118 private String extension = ""; 119 120 /** The directory that is used to hold all native libraries. */ 121 private String nativeDirectory = NATIVE; 122 123 /*--------------------------------------------------------------------------*/ 124 /** 125 * This class is implemented as a 'Singleton'. Therefore the constructor is private to prevent 126 * instantiation of this class. Use <code>getInstance()</code> to obtain an instance for use. 127 * <br> 128 * <br> 129 * For more information about the 'Singleton' pattern I highly recommend the book Design 130 * Patterns by Gamma, Helm, Johnson and Vlissides ISBN 0-201-63361-2. 131 */ 132 /*--------------------------------------------------------------------------*/ 133 private Librarian() 134 { 135 Housekeeper.getInstance().registerForCleanup(this); 136 extension = '.' + TargetFactory.getInstance().getNativeLibraryExtension(); 137 } 138 139 /*--------------------------------------------------------------------------*/ 140 /** 141 * Returns an instance of <code>Librarian</code> to use. 142 * 143 * @return an instance of <code>Librarian</code>. 144 */ 145 /*--------------------------------------------------------------------------*/ 146 public static Librarian getInstance() 147 { 148 if (me == null) 149 { 150 me = new Librarian(); 151 } 152 153 return (me); 154 } 155 156 /*--------------------------------------------------------------------------*/ 157 /** 158 * Loads the requested library. If the library is already loaded, this method returns 159 * immediately, without an attempt to load the library again. <br> 160 * <br> 161 * <b>Invocation Example:</b> This assumes that the call is made from the class that links with 162 * the library. If this is not the case, <code>this</code> must be replaced by the reference 163 * of the class that links with the library. <br> 164 * <br> 165 * <code> 166 * Librarian.getInstance ().loadLibrary ("MyLibrary", this); 167 * </code> <br> 168 * <br> 169 * Loading of a native library file works as follows:<br> 170 * <ul> 171 * <li>If the library is already loaded there is nothing to do. 172 * <li>An attempt is made to load the library by its name. If there is no system path set to 173 * the library, this attempt will fail. 174 * <li>If the client is located on the local file system, an attempt is made to load the 175 * library from the local files system as well. 176 * <li>If the library is located inside a *.jar file, it is extracted to 'java.io.tmpdir' and 177 * an attempt is made to load it from there. 178 * </ul> 179 * <br> 180 * <br> 181 * Loading from the local file system and from the *.jar file is attempted for the following 182 * potential locations of the library in this order:<br> 183 * <ol> 184 * <li>The same directory where the client is located 185 * <li>The native library directory 186 * </ol> 187 * 188 * @param name the name of the library. A file extension and path are not needed, in fact if 189 * supplied, both is stripped off. A specific extension is appended. 190 * @param client the object that made the load request 191 * 192 * @see #setNativeDirectory 193 * 194 * @exception Exception if all attempts to load the library fail. 195 */ 196 /*--------------------------------------------------------------------------*/ 197 public synchronized void loadLibrary(String name, NativeLibraryClient client) throws Exception 198 { 199 String libraryName = strip(name); 200 String tempFileName = ""; 201 202 // ---------------------------------------------------- 203 // Return if the library is already loaded 204 // ---------------------------------------------------- 205 if (loaded(libraryName)) { return; } 206 207 // ---------------------------------------------------- 208 // First try a straight load 209 // ---------------------------------------------------- 210 try 211 { 212 System.loadLibrary(libraryName); 213 return; 214 } 215 catch (UnsatisfiedLinkError exception) 216 {} 217 catch (SecurityException exception) 218 {} 219 220 // ---------------------------------------------------- 221 // Next, try to get the protocol for loading the resource. 222 // ---------------------------------------------------- 223 Class clientClass = client.getClass(); 224 String resourceName = clientClass.getName(); 225 int nameStart = resourceName.lastIndexOf('.') + 1; 226 resourceName = resourceName.substring(nameStart, resourceName.length()) + CLIENT_EXTENSION; 227 URL url = clientClass.getResource(resourceName); 228 if (url == null) { throw (new Exception("can't identify load protocol for " + libraryName 229 + extension)); } 230 String protocol = url.getProtocol(); 231 232 // ---------------------------------------------------- 233 // If it's a local file, load it from the current location 234 // ---------------------------------------------------- 235 if (protocol.equalsIgnoreCase(FILE_PROTOCOL)) 236 { 237 try 238 { 239 System.load(getClientPath(name, url)); 240 } 241 catch (Throwable exception) 242 { 243 try 244 { 245 System.load(getNativePath(name, client)); 246 } 247 catch (Throwable exception2) 248 { 249 throw (new Exception("error loading library")); 250 } 251 } 252 } 253 254 // ---------------------------------------------------- 255 // If it is in a *.jar file, extract it to 'java.io.tmpdir' 256 // ---------------------------------------------------- 257 258 else if (protocol.equalsIgnoreCase(JAR_PROTOCOL)) 259 { 260 tempFileName = getTempFileName(libraryName); 261 try 262 { 263 extractFromJar(libraryName, tempFileName, client); 264 265 clients.add(client); 266 temporaryFileNames.add(tempFileName); 267 libraryNames.add(tempFileName.substring((tempFileName 268 .lastIndexOf(File.separatorChar) + 1), tempFileName.length())); 269 270 // -------------------------------------------------- 271 // Try loading the temporary file from 'java.io.tmpdir'. 272 // -------------------------------------------------- 273 System.load(tempFileName); 274 } 275 catch (Throwable exception) 276 { 277 throw (new Exception("error loading library\n" + exception.toString())); 278 } 279 } 280 } 281 282 /*--------------------------------------------------------------------------*/ 283 /** 284 * Verifies if the library has already been loaded and keeps track of all libraries that are 285 * verified. 286 * 287 * @param name name of the library to verify 288 * 289 * @return <code>true</code> if the library had already been loaded, otherwise 290 * <code>false</code>. 291 */ 292 /*--------------------------------------------------------------------------*/ 293 private boolean loaded(String name) 294 { 295 if (trackList.contains(name)) 296 { 297 return (true); 298 } 299 else 300 { 301 trackList.add(name); 302 return (false); 303 } 304 } 305 306 /*--------------------------------------------------------------------------*/ 307 /** 308 * Strips the extension of the library name, if it has one. 309 * 310 * @param name the name of the library 311 * 312 * @return the name without an extension 313 */ 314 /*--------------------------------------------------------------------------*/ 315 private String strip(String name) 316 { 317 int extensionStart = name.lastIndexOf('.'); 318 int nameStart = name.lastIndexOf('/'); 319 if (nameStart < 0) 320 { 321 nameStart = name.lastIndexOf('\\'); 322 } 323 nameStart++; 324 325 String shortName; 326 327 if (extensionStart > 0) 328 { 329 shortName = name.substring(nameStart, extensionStart); 330 } 331 else 332 { 333 shortName = name.substring(nameStart, name.length()); 334 } 335 336 return (shortName); 337 } 338 339 /*--------------------------------------------------------------------------*/ 340 /** 341 * Makes an attempt to extract the named library from the jar file and to store it on the local 342 * file system for temporary use. If the attempt is successful, the fully qualified file name of 343 * the library on the local file system is returned. 344 * 345 * @param name the simple name of the library 346 * @param destination the fully qualified name of the destination file. 347 * @param client the class that made the load request. 348 * 349 * @exception Exception if the library can not be extracted from the *.jar file. 350 * @exception FileNotFoundException if the *.jar file does not exist. The way things operate 351 * here, this should actually never happen. 352 */ 353 /*--------------------------------------------------------------------------*/ 354 private void extractFromJar(String name, String destination, NativeLibraryClient client) 355 throws Exception 356 { 357 int bytesRead = 0; 358 OutputStream output = null; 359 360 // ---------------------------------------------------- 361 // open an input stream for the library file 362 // ---------------------------------------------------- 363 InputStream input = openInputStream(name, client); 364 365 // ---------------------------------------------------- 366 // open an output stream for the temporary file 367 // ---------------------------------------------------- 368 try 369 { 370 output = new FileOutputStream(destination); 371 } 372 catch (FileNotFoundException exception) 373 { 374 input.close(); 375 throw (new Exception("can't create destination file")); 376 } 377 catch (SecurityException exception) 378 { 379 input.close(); 380 throw (new Exception("creation of destination file denied")); 381 } 382 catch (Throwable exception) 383 { 384 input.close(); 385 throw (new Exception("unknown problem creating destination file\n" 386 + exception.toString())); 387 } 388 389 // ---------------------------------------------------- 390 // pump the data 391 // ---------------------------------------------------- 392 byte[] buffer = new byte[BLOCK_SIZE]; 393 try 394 { 395 do 396 { 397 bytesRead = input.read(buffer); 398 if (bytesRead > 0) 399 { 400 output.write(buffer, 0, bytesRead); 401 } 402 } 403 while (bytesRead > 0); 404 } 405 catch (Throwable exception) 406 { 407 throw (new Exception("error writing to destination file\n" + exception.toString())); 408 } 409 410 // ---------------------------------------------------- 411 // flush the data and close both streams 412 // ---------------------------------------------------- 413 finally 414 { 415 input.close(); 416 output.flush(); 417 output.close(); 418 } 419 } 420 421 /*--------------------------------------------------------------------------*/ 422 /** 423 * Returns the complete path (including file name) for the native library, assuming the native 424 * library is located in the same directory from which the client was loaded. 425 * 426 * @param name the simple name of the library 427 * @param clientURL a URL that points to the client class 428 * 429 * @return the path to the client 430 */ 431 /*--------------------------------------------------------------------------*/ 432 private String getClientPath(String name, URL clientURL) 433 { 434 String path = clientURL.getFile(); 435 436 int nameStart = path.lastIndexOf('/') + 1; 437 438 path = path.substring(0, nameStart); 439 path = path + name + extension; 440 path = path.replace('/', File.separatorChar); 441 // Revise the URI-path to a file path; needed in uninstaller because it 442 // writes the jar contents into a sandbox; may be with blanks in the 443 // path. 444 path = revisePath(path); 445 446 return (path); 447 } 448 449 /*--------------------------------------------------------------------------*/ 450 /** 451 * Returns the complete path (including file name) for the native library, assuming the native 452 * library is located in a directory where native libraries are ordinarily expected. 453 * 454 * @param name the simple name of the library 455 * @param client the class that made the load request. 456 * 457 * @return the path to the location of the native libraries. 458 */ 459 /*--------------------------------------------------------------------------*/ 460 private String getNativePath(String name, NativeLibraryClient client) 461 { 462 ProtectionDomain domain = client.getClass().getProtectionDomain(); 463 CodeSource codeSource = domain.getCodeSource(); 464 URL url = codeSource.getLocation(); 465 String path = url.getPath(); 466 path = path + nativeDirectory + '/' + name + extension; 467 path = path.replace('/', File.separatorChar); 468 // Revise the URI-path to a file path; needed in uninstaller because it 469 // writes the jar contents into a sandbox; may be with blanks in the 470 // path. 471 path = revisePath(path); 472 473 return (path); 474 } 475 476 /*--------------------------------------------------------------------------*/ 477 /** 478 * Revises the given path to a file compatible path. In fact this method replaces URI-like 479 * entries with it chars (e.g. %20 with a space). 480 * 481 * @param in path to be revised 482 * @return revised path 483 */ 484 /*--------------------------------------------------------------------------*/ 485 private String revisePath(String in) 486 { 487 // This was "stolen" from com.izforge.izpack.util.SelfModifier 488 489 StringBuffer sb = new StringBuffer(); 490 CharacterIterator iter = new StringCharacterIterator(in); 491 for (char c = iter.first(); c != CharacterIterator.DONE; c = iter.next()) 492 { 493 if (c == '%') 494 { 495 char c1 = iter.next(); 496 if (c1 != CharacterIterator.DONE) 497 { 498 int i1 = Character.digit(c1, 16); 499 char c2 = iter.next(); 500 if (c2 != CharacterIterator.DONE) 501 { 502 int i2 = Character.digit(c2, 16); 503 sb.append((char) ((i1 << 4) + i2)); 504 } 505 } 506 } 507 else 508 { 509 sb.append(c); 510 } 511 } 512 String path = sb.toString(); 513 return path; 514 } 515 516 /*--------------------------------------------------------------------------*/ 517 /** 518 * Opens an <code>InputStream</code> to the native library. 519 * 520 * @param name the simple name of the library 521 * @param client the class that made the load request. 522 * 523 * @return an <code>InputStream</code> from which the library can be read. 524 * 525 * @exception Exception if the library can not be located. 526 */ 527 /*--------------------------------------------------------------------------*/ 528 private InputStream openInputStream(String name, NativeLibraryClient client) throws Exception 529 { 530 Class clientClass = client.getClass(); 531 // ---------------------------------------------------- 532 // try to open an input stream, assuming the library 533 // is located with the client 534 // ---------------------------------------------------- 535 InputStream input = clientClass.getResourceAsStream(name + extension); 536 537 // ---------------------------------------------------- 538 // if this is not successful, try to load from the 539 // location where all native libraries are supposed 540 // to be located. 541 // ---------------------------------------------------- 542 if (input == null) 543 { 544 input = clientClass.getResourceAsStream('/' + nativeDirectory + '/' + name + extension); 545 } 546 547 // ---------------------------------------------------- 548 // if this fails as well, throw an exception 549 // ---------------------------------------------------- 550 if (input == null) 551 { 552 throw (new Exception("can't locate library")); 553 } 554 else 555 { 556 return (input); 557 } 558 } 559 560 /*--------------------------------------------------------------------------*/ 561 /** 562 * Builds a temporary file name for the native library. 563 * 564 * @param name the file name of the library 565 * 566 * @return a fully qualified file name that can be used to store the file on the local file 567 * system. 568 */ 569 /*--------------------------------------------------------------------------*/ 570 /* 571 * $ @design 572 * 573 * Avoid overwriting any existing files on the user's system. If by some remote chance a file by 574 * the same name should exist on the user's system, modify the temporary file name until a 575 * version is found that is unique on the system and thus won't interfere. 576 * -------------------------------------------------------------------------- 577 */ 578 private String getTempFileName(String name) 579 { 580 StringBuffer fileName = new StringBuffer(); 581 String path = System.getProperty(TEMP_LOCATION_KEY); 582 if (path.charAt(path.length() - 1) == File.separatorChar) 583 { 584 path = path.substring(0, (path.length() - 1)); 585 } 586 String modifier = ""; 587 int counter = 0; 588 File file = null; 589 590 do 591 { 592 fileName.delete(0, fileName.length()); 593 fileName.append(path); 594 fileName.append(File.separatorChar); 595 fileName.append(name); 596 fileName.append(modifier); 597 fileName.append(extension); 598 599 modifier = Integer.toString(counter); 600 counter++; 601 602 file = new File(fileName.toString()); 603 } 604 while (file.exists()); 605 606 return (fileName.toString()); 607 } 608 609 /*--------------------------------------------------------------------------*/ 610 /** 611 * Sets the directory where <code>Librarian</code> will search for native files. Directories 612 * are denoted relative to the root, where the root is the same location where the top level 613 * Java package directory is located (usually called <code>com</code>). The default directory 614 * is <code>native</code>. 615 * 616 * @param directory the directory where native files are located. 617 */ 618 /*--------------------------------------------------------------------------*/ 619 public void setNativeDirectory(String directory) 620 { 621 if (directory == null) 622 { 623 nativeDirectory = ""; 624 } 625 else 626 { 627 nativeDirectory = directory; 628 } 629 } 630 631 /*--------------------------------------------------------------------------*/ 632 /** 633 * This method attempts to remove all native libraries that have been temporarily created from 634 * the system. 635 */ 636 /*--------------------------------------------------------------------------*/ 637 public void cleanUp() 638 { 639 for (int i = 0; i < clients.size(); i++) 640 { 641 // -------------------------------------------------- 642 // free the library 643 // -------------------------------------------------- 644 NativeLibraryClient client = (NativeLibraryClient) clients.elementAt(i); 645 String libraryName = (String) libraryNames.elementAt(i); 646 647 FreeThread free = new FreeThread(libraryName, client); 648 free.start(); 649 try 650 { 651 // give the thread some time to get the library 652 // freed before attempting to delete it. 653 free.join(50); 654 } 655 catch (Throwable exception) 656 {} // nothing I can do 657 658 // -------------------------------------------------- 659 // delete the library 660 // -------------------------------------------------- 661 String tempFileName = (String) temporaryFileNames.elementAt(i); 662 try 663 { 664 File file = new File(tempFileName); 665 file.delete(); 666 } 667 catch (Throwable exception) 668 {} // nothing I can do 669 } 670 } 671} 672/*---------------------------------------------------------------------------*/