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}