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}