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}