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 090 public Library(String name) { 091 this(name, null, null); 092 } 093 094 public Library(String name, Class<?> clazz) { 095 this(name, version(clazz), clazz.getClassLoader()); 096 } 097 098 public Library(String name, String version) { 099 this(name, version, null); 100 } 101 102 public Library(String name, String version, ClassLoader classLoader) { 103 if( name == null ) { 104 throw new IllegalArgumentException("name cannot be null"); 105 } 106 this.name = name; 107 this.version = version; 108 this.classLoader= classLoader; 109 } 110 111 private static String version(Class<?> clazz) { 112 try { 113 return clazz.getPackage().getImplementationVersion(); 114 } catch (Throwable e) { 115 } 116 return null; 117 } 118 119 public static String getOperatingSystem() { 120 String name = System.getProperty("os.name").toLowerCase().trim(); 121 if( name.startsWith("linux") ) { 122 return "linux"; 123 } 124 if( name.startsWith("mac os x") ) { 125 return "osx"; 126 } 127 if( name.startsWith("win") ) { 128 return "windows"; 129 } 130 return name.replaceAll("\\W+", "_"); 131 132 } 133 134 public static String getPlatform() { 135 return getOperatingSystem()+getBitModel(); 136 } 137 138 public static int getBitModel() { 139 String prop = System.getProperty("sun.arch.data.model"); 140 if (prop == null) { 141 prop = System.getProperty("com.ibm.vm.bitmode"); 142 } 143 if( prop!=null ) { 144 return Integer.parseInt(prop); 145 } 146 return -1; // we don't know.. 147 } 148 149 /** 150 * 151 */ 152 synchronized public void load() { 153 if( loaded ) { 154 return; 155 } 156 doLoad(); 157 loaded = true; 158 } 159 160 private void doLoad() { 161 /* Perhaps a custom version is specified */ 162 String version = System.getProperty("library."+name+".version"); 163 if (version == null) { 164 version = this.version; 165 } 166 ArrayList<Throwable> errors = new ArrayList<Throwable>(); 167 168 String[] specificDirs = getSpecificSearchDirs(); 169 String libFilename = map(name); 170 String versionlibFilename = (version == null) ? null : map(name + "-" + version); 171 172 /* Try loading library from a custom library path */ 173 String customPath = System.getProperty("library."+name+".path"); 174 if (customPath != null) { 175 for ( String dir: specificDirs ) { 176 if( version!=null && load(errors, file(customPath, dir, versionlibFilename)) ) 177 return; 178 if( load(errors, file(customPath, dir, libFilename)) ) 179 return; 180 } 181 } 182 183 /* Try loading library from java library path */ 184 if( version!=null && load(errors, name + getBitModel() + "-" + version) ) 185 return; 186 if( version!=null && load(errors, name + "-" + version) ) 187 return; 188 if( load(errors, name ) ) 189 return; 190 191 192 /* Try extracting the library from the jar */ 193 if( classLoader!=null ) { 194 String targetLibName = version != null ? versionlibFilename : libFilename; 195 for ( String dir: specificDirs ) { 196 if( version!=null && extractAndLoad(errors, customPath, dir, versionlibFilename, targetLibName) ) 197 return; 198 if( extractAndLoad(errors, customPath, dir, libFilename, targetLibName) ) 199 return; 200 } 201 } 202 203 /* Failed to find the library */ 204 UnsatisfiedLinkError e = new UnsatisfiedLinkError("Could not load library. Reasons: " + errors.toString()); 205 try { 206 Method method = Throwable.class.getMethod("addSuppressed", Throwable.class); 207 for (Throwable t : errors) { 208 method.invoke(e, t); 209 } 210 } catch (Throwable ignore) { 211 } 212 throw e; 213 } 214 215 @Deprecated 216 final public String getArchSpecifcResourcePath() { 217 return getArchSpecificResourcePath(); 218 } 219 final public String getArchSpecificResourcePath() { 220 return "META-INF/native/"+ getPlatform() + "/" + System.getProperty("os.arch") + "/" +map(name); 221 } 222 223 @Deprecated 224 final public String getOperatingSystemSpecifcResourcePath() { 225 return getOperatingSystemSpecificResourcePath(); 226 } 227 final public String getOperatingSystemSpecificResourcePath() { 228 return getPlatformSpecificResourcePath(getOperatingSystem()); 229 } 230 @Deprecated 231 final public String getPlatformSpecifcResourcePath() { 232 return getPlatformSpecificResourcePath(); 233 } 234 final public String getPlatformSpecificResourcePath() { 235 return getPlatformSpecificResourcePath(getPlatform()); 236 } 237 @Deprecated 238 final public String getPlatformSpecifcResourcePath(String platform) { 239 return getPlatformSpecificResourcePath(platform); 240 } 241 final public String getPlatformSpecificResourcePath(String platform) { 242 return "META-INF/native/"+platform+"/"+map(name); 243 } 244 245 @Deprecated 246 final public String getResorucePath() { 247 return getResourcePath(); 248 } 249 final public String getResourcePath() { 250 return "META-INF/native/"+map(name); 251 } 252 253 final public String getLibraryFileName() { 254 return map(name); 255 } 256 257 /** 258 * Search directories for library:<ul> 259 * <li><code>${platform}/${arch}</code> to enable platform JNI library for different processor archs</li> 260 * <li><code>${platform}</code> to enable platform JNI library</li> 261 * <li><code>${os}</code> to enable OS JNI library</li> 262 * <li>no directory</li> 263 * </ul> 264 * @return the list 265 */ 266 final public String[] getSpecificSearchDirs() { 267 return new String[] { 268 getPlatform() + "/" + System.getProperty("os.arch"), 269 getPlatform(), 270 getOperatingSystem() 271 }; 272 } 273 274 private boolean extractAndLoad(ArrayList<Throwable> errors, String customPath, String dir, String libName, String targetLibName) { 275 String resourcePath = "META-INF/native/" + ( dir == null ? "" : (dir + '/')) + libName; 276 URL resource = classLoader.getResource(resourcePath); 277 if( resource !=null ) { 278 279 int idx = targetLibName.lastIndexOf('.'); 280 String prefix = targetLibName.substring(0, idx)+"-"; 281 String suffix = targetLibName.substring(idx); 282 283 // Use the user provided path, 284 // then fallback to the java temp directory, 285 // and last, use the user home folder 286 for (File path : Arrays.asList( 287 customPath != null ? file(customPath) : null, 288 file(System.getProperty("java.io.tmpdir")), 289 file(System.getProperty("user.home"), ".hawtjni", name))) { 290 if( path!=null ) { 291 // Try to extract it to the custom path... 292 File target = extract(errors, resource, prefix, suffix, path); 293 if( target!=null ) { 294 if( load(errors, target) ) { 295 return true; 296 } 297 } 298 } 299 } 300 } 301 return false; 302 } 303 304 private File file(String ...paths) { 305 File rc = null ; 306 for (String path : paths) { 307 if( rc == null ) { 308 rc = new File(path); 309 } else if( path != null ) { 310 rc = new File(rc, path); 311 } 312 } 313 return rc; 314 } 315 316 private String map(String libName) { 317 /* 318 * libraries in the Macintosh use the extension .jnilib but the some 319 * VMs map to .dylib. 320 */ 321 libName = System.mapLibraryName(libName); 322 String ext = ".dylib"; 323 if (libName.endsWith(ext)) { 324 libName = libName.substring(0, libName.length() - ext.length()) + ".jnilib"; 325 } 326 return libName; 327 } 328 329 private File extract(ArrayList<Throwable> errors, URL source, String prefix, String suffix, File directory) { 330 File target = null; 331 directory = directory.getAbsoluteFile(); 332 if (!directory.exists()) { 333 if (!directory.mkdirs()) { 334 errors.add(new IOException("Unable to create directory: " + directory)); 335 return null; 336 } 337 } 338 try { 339 FileOutputStream os = null; 340 InputStream is = null; 341 try { 342 target = File.createTempFile(prefix, suffix, directory); 343 is = source.openStream(); 344 if (is != null) { 345 byte[] buffer = new byte[4096]; 346 os = new FileOutputStream(target); 347 int read; 348 while ((read = is.read(buffer)) != -1) { 349 os.write(buffer, 0, read); 350 } 351 chmod755(target); 352 } 353 target.deleteOnExit(); 354 return target; 355 } finally { 356 close(os); 357 close(is); 358 } 359 } catch (Throwable e) { 360 IOException io; 361 if( target!=null ) { 362 target.delete(); 363 io = new IOException("Unable to extract library from " + source + " to " + target); 364 } else { 365 io = new IOException("Unable to create temporary file in " + directory); 366 } 367 io.initCause(e); 368 errors.add(io); 369 } 370 return null; 371 } 372 373 static private void close(Closeable file) { 374 if(file!=null) { 375 try { 376 file.close(); 377 } catch (Exception ignore) { 378 } 379 } 380 } 381 382 private void chmod755(File file) { 383 if (getPlatform().startsWith("windows")) 384 return; 385 // Use Files.setPosixFilePermissions if we are running Java 7+ to avoid forking the JVM for executing chmod 386 try { 387 ClassLoader classLoader = getClass().getClassLoader(); 388 // Check if the PosixFilePermissions exists in the JVM, if not this will throw a ClassNotFoundException 389 Class<?> posixFilePermissionsClass = classLoader.loadClass("java.nio.file.attribute.PosixFilePermissions"); 390 // Set <PosixFilePermission> permissionSet = PosixFilePermissions.fromString("rwxr-xr-x") 391 Method fromStringMethod = posixFilePermissionsClass.getMethod("fromString", String.class); 392 Object permissionSet = fromStringMethod.invoke(null, "rwxr-xr-x"); 393 // Path path = file.toPath() 394 Object path = file.getClass().getMethod("toPath").invoke(file); 395 // Files.setPosixFilePermissions(path, permissionSet) 396 Class<?> pathClass = classLoader.loadClass("java.nio.file.Path"); 397 Class<?> filesClass = classLoader.loadClass("java.nio.file.Files"); 398 Method setPosixFilePermissionsMethod = filesClass.getMethod("setPosixFilePermissions", pathClass, Set.class); 399 setPosixFilePermissionsMethod.invoke(null, path, permissionSet); 400 } catch (Throwable ignored) { 401 // Fallback to starting a new process 402 try { 403 Runtime.getRuntime().exec(new String[]{"chmod", "755", file.getCanonicalPath()}).waitFor(); 404 } catch (Throwable e) { 405 } 406 } 407 } 408 409 private boolean load(ArrayList<Throwable> errors, File lib) { 410 try { 411 System.load(lib.getPath()); 412 return true; 413 } catch (UnsatisfiedLinkError e) { 414 LinkageError le = new LinkageError("Unable to load library from " + lib); 415 le.initCause(e); 416 errors.add(le); 417 } 418 return false; 419 } 420 421 private boolean load(ArrayList<Throwable> errors, String lib) { 422 try { 423 System.loadLibrary(lib); 424 return true; 425 } catch (UnsatisfiedLinkError e) { 426 LinkageError le = new LinkageError("Unable to load library " + lib); 427 le.initCause(e); 428 errors.add(le); 429 } 430 return false; 431 } 432 433}