001/******************************************************************************* 002 * Copyright (C) 2009-2011 FuseSource Corp. 003 * Copyright (c) 2000, 2009 IBM Corporation and others. 004 * 005 * All rights reserved. This program and the accompanying materials 006 * are made available under the terms of the Eclipse Public License v1.0 007 * which accompanies this distribution, and is available at 008 * http://www.eclipse.org/legal/epl-v10.html 009 *******************************************************************************/ 010package org.fusesource.hawtjni.runtime; 011 012import java.io.*; 013import java.lang.reflect.Method; 014import java.net.URL; 015import java.security.MessageDigest; 016import java.security.NoSuchAlgorithmException; 017import java.util.ArrayList; 018import java.util.Arrays; 019import java.util.Set; 020 021/** 022 * Used to find and load a JNI library, eventually after having extracted it. 023 * 024 * It will search for the library in order at the following locations: 025 * <ol> 026 * <li> in the custom library path: If the "<code>library.${name}.path</code>" System property is set to a directory, 027 * subdirectories are searched: 028 * <ol> 029 * <li> "<code>${platform}/${arch}</code>" 030 * <li> "<code>${platform}</code>" 031 * <li> "<code>${os}</code>" 032 * <li> "<code></code>" 033 * </ol> 034 * for 2 namings of the library: 035 * <ol> 036 * <li> as "<code>${name}-${version}</code>" library name if the version can be determined. 037 * <li> as "<code>${name}</code>" library name 038 * </ol> 039 * <li> system library path: This is where the JVM looks for JNI libraries by default. 040 * <ol> 041 * <li> as "<code>${name}${bit-model}-${version}</code>" library name if the version can be determined. 042 * <li> as "<code>${name}-${version}</code>" library name if the version can be determined. 043 * <li> as "<code>${name}</code>" library name 044 * </ol> 045 * <li> classpath path: If the JNI library can be found on the classpath, it will get extracted 046 * and then loaded. This way you can embed your JNI libraries into your packaged JAR files. 047 * They are looked up as resources in this order: 048 * <ol> 049 * <li> "<code>META-INF/native/${platform}/${arch}/${library[-version]}</code>": Store your library here if you want to embed 050 * more than one platform JNI library on different processor archs in the jar. 051 * <li> "<code>META-INF/native/${platform}/${library[-version]}</code>": Store your library here if you want to embed more 052 * than one platform JNI library in the jar. 053 * <li> "<code>META-INF/native/${os}/${library[-version]}</code>": Store your library here if you want to embed more 054 * than one platform JNI library in the jar but don't want to take bit model into account. 055 * <li> "<code>META-INF/native/${library[-version]}</code>": Store your library here if your JAR is only going to embedding one 056 * platform library. 057 * </ol> 058 * The file extraction is attempted until it succeeds in the following directories. 059 * <ol> 060 * <li> The directory pointed to by the "<code>library.${name}.path</code>" System property (if set) 061 * <li> a temporary directory (uses the "<code>java.io.tmpdir</code>" System property) 062 * </ol> 063 * </ol> 064 * 065 * where: 066 * <ul> 067 * <li>"<code>${name}</code>" is the name of library 068 * <li>"<code>${version}</code>" is the value of "<code>library.${name}.version</code>" System property if set. 069 * Otherwise it is set to the ImplementationVersion property of the JAR's Manifest</li> 070 * <li>"<code>${os}</code>" is your operating system, for example "<code>osx</code>", "<code>linux</code>", or "<code>windows</code>"</li> 071 * <li>"<code>${bit-model}</code>" is "<code>64</code>" if the JVM process is a 64 bit process, otherwise it's "<code>32</code>" if the 072 * JVM is a 32 bit process</li> 073 * <li>"<code>${arch}</code>" is the architecture for the processor, for example "<code>amd64</code>" or "<code>sparcv9</code>"</li> 074 * <li>"<code>${platform}</code>" is "<code>${os}${bit-model}</code>", for example "<code>linux32</code>" or "<code>osx64</code>" </li> 075 * <li>"<code>${library[-version]}</code>": is the normal jni library name for the platform (eventually with <code>-${version}</code>) suffix. 076 * For example "<code>${name}.dll</code>" on 077 * windows, "<code>lib${name}.jnilib</code>" on OS X, and "<code>lib${name}.so</code>" on linux</li> 078 * </ul> 079 * 080 * @author <a href="http://hiramchirino.com">Hiram Chirino</a> 081 * @see System#mapLibraryName(String) 082 */ 083public class Library { 084 085 public static final String STRATEGY_PROPERTY = "hawtjni.strategy"; 086 public static final String STRATEGY_SHA1 = "sha1"; 087 public static final String STRATEGY_TEMP = "temp"; 088 089 static final String SLASH = System.getProperty("file.separator"); 090 091 static final String STRATEGY = System.getProperty(STRATEGY_PROPERTY, 092 "windows".equals(getOperatingSystem()) ? STRATEGY_SHA1 : STRATEGY_TEMP); 093 094 final private String name; 095 final private String version; 096 final private ClassLoader classLoader; 097 private boolean loaded; 098 private String nativeLibraryPath; 099 private URL nativeLibrarySourceUrl; 100 101 public Library(String name) { 102 this(name, null, null); 103 } 104 105 public Library(String name, Class<?> clazz) { 106 this(name, version(clazz), clazz.getClassLoader()); 107 } 108 109 public Library(String name, String version) { 110 this(name, version, null); 111 } 112 113 public Library(String name, String version, ClassLoader classLoader) { 114 if( name == null ) { 115 throw new IllegalArgumentException("name cannot be null"); 116 } 117 this.name = name; 118 this.version = version; 119 this.classLoader= classLoader; 120 } 121 122 private static String version(Class<?> clazz) { 123 try { 124 return clazz.getPackage().getImplementationVersion(); 125 } catch (Throwable e) { 126 } 127 return null; 128 } 129 130 /** 131 * Get the path to the native library loaded. 132 * @return the path (should not be null once the library is loaded) 133 * @since 1.16 134 */ 135 public String getNativeLibraryPath() { 136 return nativeLibraryPath; 137 } 138 139 /** 140 * Get the URL to the native library source that has been extracted (if it was extracted). 141 * @return the url to the source (in classpath) 142 * @since 1.16 143 */ 144 public URL getNativeLibrarySourceUrl() { 145 return nativeLibrarySourceUrl; 146 } 147 148 public static String getOperatingSystem() { 149 String name = System.getProperty("os.name").toLowerCase().trim(); 150 if( name.startsWith("linux") ) { 151 return "linux"; 152 } 153 if( name.startsWith("mac os x") ) { 154 return "osx"; 155 } 156 if( name.startsWith("win") ) { 157 return "windows"; 158 } 159 return name.replaceAll("\\W+", "_"); 160 161 } 162 163 public static String getPlatform() { 164 return getOperatingSystem()+getBitModel(); 165 } 166 167 public static int getBitModel() { 168 String prop = System.getProperty("sun.arch.data.model"); 169 if (prop == null) { 170 prop = System.getProperty("com.ibm.vm.bitmode"); 171 } 172 if( prop!=null ) { 173 return Integer.parseInt(prop); 174 } 175 return -1; // we don't know.. 176 } 177 178 /** 179 * Load the native library. 180 */ 181 synchronized public void load() { 182 if( loaded ) { 183 return; 184 } 185 doLoad(); 186 loaded = true; 187 } 188 189 private void doLoad() { 190 /* Perhaps a custom version is specified */ 191 String version = System.getProperty("library."+name+".version"); 192 if (version == null) { 193 version = this.version; 194 } 195 ArrayList<Throwable> errors = new ArrayList<Throwable>(); 196 197 String[] specificDirs = getSpecificSearchDirs(); 198 String libFilename = map(name); 199 String versionlibFilename = (version == null) ? null : map(name + "-" + version); 200 201 /* Try loading library from a custom library path */ 202 String customPath = System.getProperty("library."+name+".path"); 203 if (customPath != null) { 204 for ( String dir: specificDirs ) { 205 if( version!=null && load(errors, file(customPath, dir, versionlibFilename)) ) 206 return; 207 if( load(errors, file(customPath, dir, libFilename)) ) 208 return; 209 } 210 } 211 212 /* Try loading library from java library path */ 213 if( version!=null && loadLibrary(errors, name + getBitModel() + "-" + version) ) 214 return; 215 if( version!=null && loadLibrary(errors, name + "-" + version) ) 216 return; 217 if( loadLibrary(errors, name) ) 218 return; 219 220 221 /* Try extracting the library from the jar */ 222 if( classLoader!=null ) { 223 String targetLibName = version != null ? versionlibFilename : libFilename; 224 for ( String dir: specificDirs ) { 225 if( version!=null && extractAndLoad(errors, customPath, dir, versionlibFilename, targetLibName) ) 226 return; 227 if( extractAndLoad(errors, customPath, dir, libFilename, targetLibName) ) 228 return; 229 } 230 } 231 232 /* Failed to find the library */ 233 UnsatisfiedLinkError e = new UnsatisfiedLinkError("Could not load library. Reasons: " + errors.toString()); 234 try { 235 Method method = Throwable.class.getMethod("addSuppressed", Throwable.class); 236 for (Throwable t : errors) { 237 method.invoke(e, t); 238 } 239 } catch (Throwable ignore) { 240 } 241 throw e; 242 } 243 244 @Deprecated 245 final public String getArchSpecifcResourcePath() { 246 return getArchSpecificResourcePath(); 247 } 248 final public String getArchSpecificResourcePath() { 249 return "META-INF/native/"+ getPlatform() + "/" + System.getProperty("os.arch") + "/" +map(name); 250 } 251 252 @Deprecated 253 final public String getOperatingSystemSpecifcResourcePath() { 254 return getOperatingSystemSpecificResourcePath(); 255 } 256 final public String getOperatingSystemSpecificResourcePath() { 257 return getPlatformSpecificResourcePath(getOperatingSystem()); 258 } 259 @Deprecated 260 final public String getPlatformSpecifcResourcePath() { 261 return getPlatformSpecificResourcePath(); 262 } 263 final public String getPlatformSpecificResourcePath() { 264 return getPlatformSpecificResourcePath(getPlatform()); 265 } 266 @Deprecated 267 final public String getPlatformSpecifcResourcePath(String platform) { 268 return getPlatformSpecificResourcePath(platform); 269 } 270 final public String getPlatformSpecificResourcePath(String platform) { 271 return "META-INF/native/"+platform+"/"+map(name); 272 } 273 274 @Deprecated 275 final public String getResorucePath() { 276 return getResourcePath(); 277 } 278 final public String getResourcePath() { 279 return "META-INF/native/"+map(name); 280 } 281 282 final public String getLibraryFileName() { 283 return map(name); 284 } 285 286 /** 287 * Search directories for library:<ul> 288 * <li><code>${platform}/${arch}</code> to enable platform JNI library for different processor archs</li> 289 * <li><code>${platform}</code> to enable platform JNI library</li> 290 * <li><code>${os}</code> to enable OS JNI library</li> 291 * <li>no directory</li> 292 * </ul> 293 * @return the list 294 * @since 1.15 295 */ 296 final public String[] getSpecificSearchDirs() { 297 return new String[] { 298 getPlatform() + "/" + System.getProperty("os.arch"), 299 getPlatform(), 300 getOperatingSystem(), 301 "." 302 }; 303 } 304 305 private boolean extractAndLoad(ArrayList<Throwable> errors, String customPath, String dir, String libName, String targetLibName) { 306 String resourcePath = "META-INF/native/" + ( dir == null ? "" : (dir + '/')) + libName; 307 URL resource = classLoader.getResource(resourcePath); 308 if( resource !=null ) { 309 310 int idx = targetLibName.lastIndexOf('.'); 311 String prefix = targetLibName.substring(0, idx)+"-"; 312 String suffix = targetLibName.substring(idx); 313 314 // Use the user provided path, 315 // then fallback to the java temp directory, 316 // and last, use the user home folder 317 for (File path : Arrays.asList( 318 customPath != null ? file(customPath) : null, 319 file(System.getProperty("java.io.tmpdir")), 320 file(System.getProperty("user.home"), ".hawtjni", name))) { 321 if( path!=null ) { 322 // Try to extract it to the custom path... 323 File target; 324 if (STRATEGY_SHA1.equals(STRATEGY)) { 325 target = extractSha1(errors, resource, prefix, suffix, path); 326 } else { 327 target = extractTemp(errors, resource, prefix, suffix, path); 328 } 329 if( target!=null ) { 330 if( load(errors, target) ) { 331 nativeLibrarySourceUrl = resource; 332 return true; 333 } 334 } 335 } 336 } 337 } 338 return false; 339 } 340 341 private File file(String ...paths) { 342 File rc = null ; 343 for (String path : paths) { 344 if( rc == null ) { 345 rc = new File(path); 346 } else if( path != null ) { 347 rc = new File(rc, path); 348 } 349 } 350 return rc; 351 } 352 353 private String map(String libName) { 354 /* 355 * libraries in the Macintosh use the extension .jnilib but the some 356 * VMs map to .dylib. 357 */ 358 libName = System.mapLibraryName(libName); 359 String ext = ".dylib"; 360 if (libName.endsWith(ext)) { 361 libName = libName.substring(0, libName.length() - ext.length()) + ".jnilib"; 362 } 363 return libName; 364 } 365 366 private File extractSha1(ArrayList<Throwable> errors, URL source, String prefix, String suffix, File directory) { 367 File target = null; 368 directory = directory.getAbsoluteFile(); 369 if (!directory.exists()) { 370 if (!directory.mkdirs()) { 371 errors.add(new IOException("Unable to create directory: " + directory)); 372 return null; 373 } 374 } 375 try { 376 String sha1 = computeSha1(source.openStream()); 377 String sha1f = ""; 378 target = new File(directory, prefix + sha1 + suffix); 379 380 if (target.isFile() && target.canRead()) { 381 sha1f = computeSha1(new FileInputStream(target)); 382 } 383 if (sha1f.equals(sha1)) { 384 return target; 385 } 386 387 FileOutputStream os = null; 388 InputStream is = null; 389 try { 390 is = source.openStream(); 391 if (is != null) { 392 byte[] buffer = new byte[4096]; 393 os = new FileOutputStream(target); 394 int read; 395 while ((read = is.read(buffer)) != -1) { 396 os.write(buffer, 0, read); 397 } 398 chmod755(target); 399 } 400 return target; 401 } finally { 402 close(os); 403 close(is); 404 } 405 } catch (Throwable e) { 406 IOException io; 407 if (target != null) { 408 target.delete(); 409 io = new IOException("Unable to extract library from " + source + " to " + target); 410 } else { 411 io = new IOException("Unable to create temporary file in " + directory); 412 } 413 io.initCause(e); 414 errors.add(io); 415 } 416 return null; 417 } 418 419 private String computeSha1(InputStream is) throws NoSuchAlgorithmException, IOException { 420 String sha1; 421 try { 422 MessageDigest mDigest = MessageDigest.getInstance("SHA1"); 423 int read; 424 byte[] buffer = new byte[4096]; 425 while ((read = is.read(buffer)) != -1) { 426 mDigest.update(buffer, 0, read); 427 } 428 byte[] result = mDigest.digest(); 429 StringBuilder sb = new StringBuilder(); 430 for (byte b : result) { 431 sb.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1)); 432 } 433 sha1 = sb.toString(); 434 } finally { 435 close(is); 436 } 437 return sha1; 438 } 439 440 private File extractTemp(ArrayList<Throwable> errors, URL source, String prefix, String suffix, File directory) { 441 File target = null; 442 directory = directory.getAbsoluteFile(); 443 if (!directory.exists()) { 444 if (!directory.mkdirs()) { 445 errors.add(new IOException("Unable to create directory: " + directory)); 446 return null; 447 } 448 } 449 try { 450 FileOutputStream os = null; 451 InputStream is = null; 452 try { 453 target = File.createTempFile(prefix, suffix, directory); 454 is = source.openStream(); 455 if (is != null) { 456 byte[] buffer = new byte[4096]; 457 os = new FileOutputStream(target); 458 int read; 459 while ((read = is.read(buffer)) != -1) { 460 os.write(buffer, 0, read); 461 } 462 chmod755(target); 463 } 464 target.deleteOnExit(); 465 return target; 466 } finally { 467 close(os); 468 close(is); 469 } 470 } catch (Throwable e) { 471 IOException io; 472 if (target != null) { 473 target.delete(); 474 io = new IOException("Unable to extract library from " + source + " to " + target); 475 } else { 476 io = new IOException("Unable to create temporary file in " + directory); 477 } 478 io.initCause(e); 479 errors.add(io); 480 } 481 return null; 482 } 483 484 static private void close(Closeable file) { 485 if (file != null) { 486 try { 487 file.close(); 488 } catch (Exception ignore) { 489 } 490 } 491 } 492 493 private void chmod755(File file) { 494 if (getPlatform().startsWith("windows")) 495 return; 496 // Use Files.setPosixFilePermissions if we are running Java 7+ to avoid forking the JVM for executing chmod 497 try { 498 ClassLoader classLoader = getClass().getClassLoader(); 499 // Check if the PosixFilePermissions exists in the JVM, if not this will throw a ClassNotFoundException 500 Class<?> posixFilePermissionsClass = classLoader.loadClass("java.nio.file.attribute.PosixFilePermissions"); 501 // Set <PosixFilePermission> permissionSet = PosixFilePermissions.fromString("rwxr-xr-x") 502 Method fromStringMethod = posixFilePermissionsClass.getMethod("fromString", String.class); 503 Object permissionSet = fromStringMethod.invoke(null, "rwxr-xr-x"); 504 // Path path = file.toPath() 505 Object path = file.getClass().getMethod("toPath").invoke(file); 506 // Files.setPosixFilePermissions(path, permissionSet) 507 Class<?> pathClass = classLoader.loadClass("java.nio.file.Path"); 508 Class<?> filesClass = classLoader.loadClass("java.nio.file.Files"); 509 Method setPosixFilePermissionsMethod = filesClass.getMethod("setPosixFilePermissions", pathClass, Set.class); 510 setPosixFilePermissionsMethod.invoke(null, path, permissionSet); 511 } catch (Throwable ignored) { 512 // Fallback to starting a new process 513 try { 514 Runtime.getRuntime().exec(new String[]{"chmod", "755", file.getCanonicalPath()}).waitFor(); 515 } catch (Throwable e) { 516 } 517 } 518 } 519 520 private boolean load(ArrayList<Throwable> errors, File lib) { 521 try { 522 System.load(lib.getPath()); 523 nativeLibraryPath = lib.getPath(); 524 return true; 525 } catch (UnsatisfiedLinkError e) { 526 LinkageError le = new LinkageError("Unable to load library from " + lib); 527 le.initCause(e); 528 errors.add(le); 529 } 530 return false; 531 } 532 533 private boolean loadLibrary(ArrayList<Throwable> errors, String lib) { 534 try { 535 System.loadLibrary(lib); 536 nativeLibraryPath = "java.library.path,sun.boot.library.pathlib:" + lib; 537 return true; 538 } catch (UnsatisfiedLinkError e) { 539 LinkageError le = new LinkageError("Unable to load library " + lib); 540 le.initCause(e); 541 errors.add(le); 542 } 543 return false; 544 } 545 546}