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